mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 08:24:40 +01:00
refactor: move xontrib.jedi out
This commit is contained in:
parent
48086a5778
commit
43cbc770e9
3 changed files with 19 additions and 436 deletions
|
@ -1,261 +0,0 @@
|
||||||
"""Tests for the Jedi completer xontrib"""
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
from unittest.mock import MagicMock, call
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from tests.tools import skip_if_on_darwin, skip_if_on_windows
|
|
||||||
from xonsh.completers.tools import RichCompletion
|
|
||||||
from xonsh.parsers.completion_context import CompletionContext, PythonContext
|
|
||||||
from xonsh.xontribs import find_xontrib
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def jedi_mock(monkeypatch):
|
|
||||||
jedi_mock = MagicMock()
|
|
||||||
jedi_mock.__version__ = "0.16.0"
|
|
||||||
jedi_mock.Interpreter().complete.return_value = []
|
|
||||||
jedi_mock.reset_mock()
|
|
||||||
monkeypatch.setitem(sys.modules, "jedi", jedi_mock)
|
|
||||||
yield jedi_mock
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def completer_mock(monkeypatch, xession):
|
|
||||||
completer_mock = MagicMock()
|
|
||||||
|
|
||||||
# so that args will be passed
|
|
||||||
def comp(args):
|
|
||||||
completer_mock(args)
|
|
||||||
|
|
||||||
monkeypatch.setitem(xession.aliases, "completer", comp)
|
|
||||||
yield completer_mock
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def jedi_xontrib(monkeypatch, source_path, jedi_mock, completer_mock):
|
|
||||||
monkeypatch.syspath_prepend(source_path)
|
|
||||||
spec = find_xontrib("jedi")
|
|
||||||
yield importlib.import_module(spec.name)
|
|
||||||
del sys.modules[spec.name]
|
|
||||||
|
|
||||||
|
|
||||||
def test_completer_added(jedi_xontrib, xession):
|
|
||||||
assert "xontrib.jedi" in sys.modules
|
|
||||||
assert "python" not in xession.completers
|
|
||||||
assert "python_mode" not in xession.completers
|
|
||||||
assert "jedi_python" in xession.completers
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"context",
|
|
||||||
[
|
|
||||||
CompletionContext(python=PythonContext("10 + x", 6)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize("version", ["new", "old"])
|
|
||||||
def test_jedi_api(jedi_xontrib, jedi_mock, version, context, xession):
|
|
||||||
if version == "old":
|
|
||||||
jedi_mock.__version__ = "0.15.0"
|
|
||||||
jedi_mock.Interpreter().completions.return_value = []
|
|
||||||
jedi_mock.reset_mock()
|
|
||||||
|
|
||||||
jedi_xontrib.complete_jedi(context)
|
|
||||||
|
|
||||||
extra_namespace = {"__xonsh__": xession}
|
|
||||||
try:
|
|
||||||
extra_namespace["_"] = _
|
|
||||||
except NameError:
|
|
||||||
pass
|
|
||||||
namespaces = [{}, extra_namespace]
|
|
||||||
|
|
||||||
line = context.python.multiline_code
|
|
||||||
end = context.python.cursor_index
|
|
||||||
if version == "new":
|
|
||||||
assert jedi_mock.Interpreter.call_args_list == [call(line, namespaces)]
|
|
||||||
assert jedi_mock.Interpreter().complete.call_args_list == [call(1, end)]
|
|
||||||
else:
|
|
||||||
assert jedi_mock.Interpreter.call_args_list == [
|
|
||||||
call(line, namespaces, line=1, column=end)
|
|
||||||
]
|
|
||||||
assert jedi_mock.Interpreter().completions.call_args_list == [call()]
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiline(jedi_xontrib, jedi_mock, monkeypatch):
|
|
||||||
complete_document = "xx = 1\n1 + x"
|
|
||||||
jedi_xontrib.complete_jedi(
|
|
||||||
CompletionContext(
|
|
||||||
python=PythonContext(complete_document, len(complete_document))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert jedi_mock.Interpreter.call_args_list[0][0][0] == complete_document
|
|
||||||
assert jedi_mock.Interpreter().complete.call_args_list == [
|
|
||||||
call(2, 5) # line (one-indexed), column (zero-indexed)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"completion, rich_completion",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
# from jedi when code is 'x' and xx=3
|
|
||||||
(
|
|
||||||
"instance",
|
|
||||||
"xx",
|
|
||||||
"x",
|
|
||||||
"int(x=None, /) -> int",
|
|
||||||
("instance", "instance int"),
|
|
||||||
),
|
|
||||||
RichCompletion(
|
|
||||||
"xx", display="xx", description="instance int", prefix_len=1
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from jedi when code is 'xx=3\nx'
|
|
||||||
("statement", "xx", "x", None, ("instance", "instance int")),
|
|
||||||
RichCompletion(
|
|
||||||
"xx", display="xx", description="instance int", prefix_len=1
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from jedi when code is 'x.' and x=3
|
|
||||||
(
|
|
||||||
"function",
|
|
||||||
"from_bytes",
|
|
||||||
"from_bytes",
|
|
||||||
"from_bytes(bytes, byteorder, *, signed=False)",
|
|
||||||
("function", "def __get__"),
|
|
||||||
),
|
|
||||||
RichCompletion(
|
|
||||||
"from_bytes",
|
|
||||||
display="from_bytes()",
|
|
||||||
description="from_bytes(bytes, byteorder, *, signed=False)",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from jedi when code is 'x=3\nx.'
|
|
||||||
("function", "imag", "imag", None, ("instance", "instance int")),
|
|
||||||
RichCompletion("imag", display="imag", description="instance int"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from '(3).from_bytes(byt'
|
|
||||||
("param", "bytes=", "es=", None, ("instance", "instance Sequence")),
|
|
||||||
RichCompletion(
|
|
||||||
"bytes=",
|
|
||||||
display="bytes=",
|
|
||||||
description="instance Sequence",
|
|
||||||
prefix_len=3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'x.from_bytes(byt' when x=3
|
|
||||||
("param", "bytes=", "es=", None, None),
|
|
||||||
RichCompletion(
|
|
||||||
"bytes=", display="bytes=", description="param", prefix_len=3
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'import colle'
|
|
||||||
("module", "collections", "ctions", None, ("module", "module collections")),
|
|
||||||
RichCompletion(
|
|
||||||
"collections",
|
|
||||||
display="collections",
|
|
||||||
description="module collections",
|
|
||||||
prefix_len=5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'NameErr'
|
|
||||||
(
|
|
||||||
"class",
|
|
||||||
"NameError",
|
|
||||||
"or",
|
|
||||||
"NameError(*args: object)",
|
|
||||||
("class", "class NameError"),
|
|
||||||
),
|
|
||||||
RichCompletion(
|
|
||||||
"NameError",
|
|
||||||
display="NameError",
|
|
||||||
description="NameError(*args: object)",
|
|
||||||
prefix_len=7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'a["' when a={'name':None}
|
|
||||||
("string", '"name"', 'name"', None, None),
|
|
||||||
RichCompletion('"name"', display='"name"', description="string"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'open("/etc/pass'
|
|
||||||
("path", 'passwd"', 'wd"', None, None),
|
|
||||||
RichCompletion(
|
|
||||||
'passwd"', display='passwd"', description="path", prefix_len=4
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# from 'cla'
|
|
||||||
("keyword", "class", "ss", None, None),
|
|
||||||
RichCompletion(
|
|
||||||
"class", display="class", description="keyword", prefix_len=3
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_rich_completions(jedi_xontrib, jedi_mock, completion, rich_completion):
|
|
||||||
comp_type, comp_name, comp_complete, sig, inf = completion
|
|
||||||
comp_mock = MagicMock()
|
|
||||||
comp_mock.type = comp_type
|
|
||||||
comp_mock.name = comp_name
|
|
||||||
comp_mock.complete = comp_complete
|
|
||||||
if sig:
|
|
||||||
sig_mock = MagicMock()
|
|
||||||
sig_mock.to_string.return_value = sig
|
|
||||||
comp_mock.get_signatures.return_value = [sig_mock]
|
|
||||||
else:
|
|
||||||
comp_mock.get_signatures.return_value = []
|
|
||||||
if inf:
|
|
||||||
inf_type, inf_desc = inf
|
|
||||||
inf_mock = MagicMock()
|
|
||||||
inf_mock.type = inf_type
|
|
||||||
inf_mock.description = inf_desc
|
|
||||||
comp_mock.infer.return_value = [inf_mock]
|
|
||||||
else:
|
|
||||||
comp_mock.infer.return_value = []
|
|
||||||
|
|
||||||
jedi_xontrib.XONSH_SPECIAL_TOKENS = []
|
|
||||||
jedi_mock.Interpreter().complete.return_value = [comp_mock]
|
|
||||||
completions = jedi_xontrib.complete_jedi(
|
|
||||||
CompletionContext(python=PythonContext("", 0))
|
|
||||||
)
|
|
||||||
assert len(completions) == 1
|
|
||||||
(ret_completion,) = completions
|
|
||||||
assert isinstance(ret_completion, RichCompletion)
|
|
||||||
assert ret_completion == rich_completion
|
|
||||||
assert ret_completion.display == rich_completion.display
|
|
||||||
assert ret_completion.description == rich_completion.description
|
|
||||||
|
|
||||||
|
|
||||||
def test_special_tokens(jedi_xontrib):
|
|
||||||
assert jedi_xontrib.complete_jedi(
|
|
||||||
CompletionContext(python=PythonContext("", 0))
|
|
||||||
).issuperset(jedi_xontrib.XONSH_SPECIAL_TOKENS)
|
|
||||||
assert jedi_xontrib.complete_jedi(
|
|
||||||
CompletionContext(python=PythonContext("@", 1))
|
|
||||||
) == {"@", "@(", "@$("}
|
|
||||||
assert jedi_xontrib.complete_jedi(
|
|
||||||
CompletionContext(python=PythonContext("$", 1))
|
|
||||||
) == {"$[", "${", "$("}
|
|
||||||
|
|
||||||
|
|
||||||
@skip_if_on_darwin
|
|
||||||
@skip_if_on_windows
|
|
||||||
def test_no_command_path_completion(jedi_xontrib, completion_context_parse):
|
|
||||||
assert jedi_xontrib.complete_jedi(completion_context_parse("./", 2)) is None
|
|
||||||
assert jedi_xontrib.complete_jedi(completion_context_parse("~/", 2)) is None
|
|
||||||
assert jedi_xontrib.complete_jedi(completion_context_parse("./e", 3)) is None
|
|
||||||
assert jedi_xontrib.complete_jedi(completion_context_parse("/usr/bin/", 9)) is None
|
|
||||||
assert (
|
|
||||||
jedi_xontrib.complete_jedi(completion_context_parse("/usr/bin/e", 10)) is None
|
|
||||||
)
|
|
|
@ -81,6 +81,19 @@ def define_xontribs():
|
||||||
},
|
},
|
||||||
url="http://xon.sh",
|
url="http://xon.sh",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_xontrib(name: str, desc: str):
|
||||||
|
return Xontrib(
|
||||||
|
url=f"https://github.com/xonsh/xontrib-{name}",
|
||||||
|
description=desc,
|
||||||
|
package=_XontribPkg(
|
||||||
|
name="xonsh-{name}",
|
||||||
|
license="BSD 2-clause",
|
||||||
|
install={"pifp": "xpip install xonsh-{name}"},
|
||||||
|
url=f"https://github.com/xonsh/xontrib-{name}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"abbrevs": Xontrib(
|
"abbrevs": Xontrib(
|
||||||
url="http://xon.sh",
|
url="http://xon.sh",
|
||||||
|
@ -414,10 +427,9 @@ def define_xontribs():
|
||||||
url="https://github.com/eugenesvk/xontrib-homebrew",
|
url="https://github.com/eugenesvk/xontrib-homebrew",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"jedi": Xontrib(
|
"jedi": get_xontrib(
|
||||||
url="http://xon.sh",
|
"jedi",
|
||||||
description="Use Jedi as xonsh's python completer.",
|
"Use Jedi as xonsh's python completer.",
|
||||||
package=core_pkg,
|
|
||||||
),
|
),
|
||||||
"kitty": Xontrib(
|
"kitty": Xontrib(
|
||||||
url="https://github.com/scopatz/xontrib-kitty",
|
url="https://github.com/scopatz/xontrib-kitty",
|
||||||
|
@ -442,11 +454,10 @@ def define_xontribs():
|
||||||
url="https://github.com/anki-code/xontrib-macro-lib",
|
url="https://github.com/anki-code/xontrib-macro-lib",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"mpl": Xontrib(
|
"mpl": get_xontrib(
|
||||||
url="http://xon.sh",
|
"mpl",
|
||||||
description="Matplotlib hooks for xonsh, including the new 'mpl' "
|
"Matplotlib hooks for xonsh, including the new 'mpl' "
|
||||||
"alias that displays the current figure on the screen.",
|
"alias that displays the current figure on the screen.",
|
||||||
package=core_pkg,
|
|
||||||
),
|
),
|
||||||
"onepath": Xontrib(
|
"onepath": Xontrib(
|
||||||
url="https://github.com/anki-code/xontrib-onepath",
|
url="https://github.com/anki-code/xontrib-onepath",
|
||||||
|
|
167
xontrib/jedi.py
167
xontrib/jedi.py
|
@ -1,167 +0,0 @@
|
||||||
"""Use Jedi as xonsh's python completer."""
|
|
||||||
import os
|
|
||||||
|
|
||||||
import xonsh
|
|
||||||
from xonsh.built_ins import XSH
|
|
||||||
from xonsh.completers import _aliases
|
|
||||||
from xonsh.completers.tools import (
|
|
||||||
RichCompletion,
|
|
||||||
contextual_completer,
|
|
||||||
get_filter_function,
|
|
||||||
)
|
|
||||||
from xonsh.lazyasd import lazybool, lazyobject
|
|
||||||
from xonsh.parsers.completion_context import CompletionContext
|
|
||||||
|
|
||||||
__all__ = ()
|
|
||||||
|
|
||||||
# an error will be printed in xontribs
|
|
||||||
# if jedi isn't installed
|
|
||||||
import jedi
|
|
||||||
|
|
||||||
|
|
||||||
@lazybool
|
|
||||||
def JEDI_NEW_API():
|
|
||||||
if hasattr(jedi, "__version__"):
|
|
||||||
return tuple(map(int, jedi.__version__.split("."))) >= (0, 16, 0)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@lazyobject
|
|
||||||
def XONSH_SPECIAL_TOKENS():
|
|
||||||
return {
|
|
||||||
"?",
|
|
||||||
"??",
|
|
||||||
"$(",
|
|
||||||
"${",
|
|
||||||
"$[",
|
|
||||||
"![",
|
|
||||||
"!(",
|
|
||||||
"@(",
|
|
||||||
"@$(",
|
|
||||||
"@",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@lazyobject
|
|
||||||
def XONSH_SPECIAL_TOKENS_FIRST():
|
|
||||||
return {tok[0] for tok in XONSH_SPECIAL_TOKENS}
|
|
||||||
|
|
||||||
|
|
||||||
@contextual_completer
|
|
||||||
def complete_jedi(context: CompletionContext):
|
|
||||||
"""Completes python code using Jedi and xonsh operators"""
|
|
||||||
if context.python is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ctx = context.python.ctx or {}
|
|
||||||
|
|
||||||
# if the first word is a known command (and we're not completing it), don't complete.
|
|
||||||
# taken from xonsh/completers/python.py
|
|
||||||
if context.command and context.command.arg_index != 0:
|
|
||||||
first = context.command.args[0].value
|
|
||||||
if first in XSH.commands_cache and first not in ctx: # type: ignore
|
|
||||||
return None
|
|
||||||
|
|
||||||
# if we're completing a possible command and the prefix contains a valid path, don't complete.
|
|
||||||
if context.command:
|
|
||||||
path_dir = os.path.dirname(context.command.prefix)
|
|
||||||
if path_dir and os.path.isdir(os.path.expanduser(path_dir)):
|
|
||||||
return None
|
|
||||||
|
|
||||||
filter_func = get_filter_function()
|
|
||||||
jedi.settings.case_insensitive_completion = not XSH.env.get(
|
|
||||||
"CASE_SENSITIVE_COMPLETIONS"
|
|
||||||
)
|
|
||||||
|
|
||||||
source = context.python.multiline_code
|
|
||||||
index = context.python.cursor_index
|
|
||||||
row = source.count("\n", 0, index) + 1
|
|
||||||
column = (
|
|
||||||
index - source.rfind("\n", 0, index) - 1
|
|
||||||
) # will be `index - (-1) - 1` if there's no newline
|
|
||||||
|
|
||||||
extra_ctx = {"__xonsh__": XSH}
|
|
||||||
try:
|
|
||||||
extra_ctx["_"] = _
|
|
||||||
except NameError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if JEDI_NEW_API:
|
|
||||||
script = jedi.Interpreter(source, [ctx, extra_ctx])
|
|
||||||
else:
|
|
||||||
script = jedi.Interpreter(source, [ctx, extra_ctx], line=row, column=column)
|
|
||||||
|
|
||||||
script_comp = set()
|
|
||||||
try:
|
|
||||||
if JEDI_NEW_API:
|
|
||||||
script_comp = script.complete(row, column)
|
|
||||||
else:
|
|
||||||
script_comp = script.completions()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
res = {create_completion(comp) for comp in script_comp if should_complete(comp)}
|
|
||||||
|
|
||||||
if index > 0:
|
|
||||||
last_char = source[index - 1]
|
|
||||||
res.update(
|
|
||||||
RichCompletion(t, prefix_len=1)
|
|
||||||
for t in XONSH_SPECIAL_TOKENS
|
|
||||||
if filter_func(t, last_char)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
res.update(RichCompletion(t, prefix_len=0) for t in XONSH_SPECIAL_TOKENS)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def should_complete(comp: jedi.api.classes.Completion):
|
|
||||||
"""
|
|
||||||
make sure _* names are completed only when
|
|
||||||
the user writes the first underscore
|
|
||||||
"""
|
|
||||||
name = comp.name
|
|
||||||
if not name.startswith("_"):
|
|
||||||
return True
|
|
||||||
completion = comp.complete
|
|
||||||
# only if we're not completing the first underscore:
|
|
||||||
return completion and len(completion) <= len(name) - 1
|
|
||||||
|
|
||||||
|
|
||||||
def create_completion(comp: jedi.api.classes.Completion):
|
|
||||||
"""Create a RichCompletion from a Jedi Completion object"""
|
|
||||||
comp_type = None
|
|
||||||
description = None
|
|
||||||
|
|
||||||
if comp.type != "instance":
|
|
||||||
sigs = comp.get_signatures()
|
|
||||||
if sigs:
|
|
||||||
comp_type = comp.type
|
|
||||||
description = sigs[0].to_string()
|
|
||||||
if comp_type is None:
|
|
||||||
# jedi doesn't know exactly what this is
|
|
||||||
inf = comp.infer()
|
|
||||||
if inf:
|
|
||||||
comp_type = inf[0].type
|
|
||||||
description = inf[0].description
|
|
||||||
|
|
||||||
display = comp.name + ("()" if comp_type == "function" else "")
|
|
||||||
description = description or comp.type
|
|
||||||
|
|
||||||
prefix_len = len(comp.name) - len(comp.complete)
|
|
||||||
|
|
||||||
return RichCompletion(
|
|
||||||
comp.name,
|
|
||||||
display=display,
|
|
||||||
description=description,
|
|
||||||
prefix_len=prefix_len,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# monkey-patch the original python completer in 'base'.
|
|
||||||
xonsh.completers.base.complete_python = complete_jedi
|
|
||||||
|
|
||||||
# Jedi ignores leading '@(' and friends
|
|
||||||
_aliases._add_one_completer("jedi_python", complete_jedi, "<python")
|
|
||||||
_aliases._remove_completer(["python"])
|
|
Loading…
Add table
Reference in a new issue