diff --git a/tests/xontribs/test_jedi.py b/tests/xontribs/test_jedi.py deleted file mode 100644 index 712c0209c..000000000 --- a/tests/xontribs/test_jedi.py +++ /dev/null @@ -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 - ) diff --git a/xonsh/xontribs_meta.py b/xonsh/xontribs_meta.py index 49622ecad..b48d7c677 100644 --- a/xonsh/xontribs_meta.py +++ b/xonsh/xontribs_meta.py @@ -81,6 +81,19 @@ def define_xontribs(): }, 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 { "abbrevs": Xontrib( url="http://xon.sh", @@ -414,10 +427,9 @@ def define_xontribs(): url="https://github.com/eugenesvk/xontrib-homebrew", ), ), - "jedi": Xontrib( - url="http://xon.sh", - description="Use Jedi as xonsh's python completer.", - package=core_pkg, + "jedi": get_xontrib( + "jedi", + "Use Jedi as xonsh's python completer.", ), "kitty": Xontrib( url="https://github.com/scopatz/xontrib-kitty", @@ -442,11 +454,10 @@ def define_xontribs(): url="https://github.com/anki-code/xontrib-macro-lib", ), ), - "mpl": Xontrib( - url="http://xon.sh", - description="Matplotlib hooks for xonsh, including the new 'mpl' " + "mpl": get_xontrib( + "mpl", + "Matplotlib hooks for xonsh, including the new 'mpl' " "alias that displays the current figure on the screen.", - package=core_pkg, ), "onepath": Xontrib( url="https://github.com/anki-code/xontrib-onepath", diff --git a/xontrib/jedi.py b/xontrib/jedi.py deleted file mode 100644 index 24cb1fb11..000000000 --- a/xontrib/jedi.py +++ /dev/null @@ -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, "