Files
jedi/test/test_api/test_api.py
Peter Law 5ca69458d4 Add testing for mismatch cases
This should help catch any errors in our handling of invalid cases.
While some of these produce outputs which aren't correct, what
we're checking here is that we don't _error_ while producing that
output.

Also fix a case which this showed up.
2020-03-22 15:10:43 +00:00

375 lines
11 KiB
Python

"""
Test all things related to the ``jedi.api`` module.
"""
import os
import sys
from textwrap import dedent
import pytest
from pytest import raises
from parso import cache
from jedi._compatibility import unicode
from jedi import preload_module
from jedi.inference.gradual import typeshed
from test.helpers import test_dir, get_example_dir
@pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, EoL")
def test_preload_modules():
def check_loaded(*modules):
for grammar_cache in cache.parser_cache.values():
if None in grammar_cache:
break
# Filter the typeshed parser cache.
typeshed_cache_count = sum(
1 for path in grammar_cache
if path is not None and path.startswith(typeshed.TYPESHED_PATH)
)
# +1 for None module (currently used)
assert len(grammar_cache) - typeshed_cache_count == len(modules) + 1
for i in modules:
assert [i in k for k in grammar_cache.keys() if k is not None]
old_cache = cache.parser_cache.copy()
cache.parser_cache.clear()
try:
preload_module('sys')
check_loaded() # compiled (c_builtin) modules shouldn't be in the cache.
preload_module('types', 'token')
check_loaded('types', 'token')
finally:
cache.parser_cache.update(old_cache)
def test_empty_script(Script):
assert Script('')
def test_line_number_errors(Script):
"""
Script should raise a ValueError if line/column numbers are not in a
valid range.
"""
s = 'hello'
# lines
with raises(ValueError):
Script(s).complete(2, 0)
with raises(ValueError):
Script(s).complete(0, 0)
# columns
with raises(ValueError):
Script(s).infer(1, len(s) + 1)
with raises(ValueError):
Script(s).goto(1, -1)
# ok
Script(s).get_signatures(1, 0)
Script(s).get_references(1, len(s))
def _check_number(Script, source, result='float'):
completions = Script(source).complete()
assert completions[0].parent().name == result
def test_completion_on_number_literals(Script):
# No completions on an int literal (is a float).
assert [c.name for c in Script('1. ').complete()] \
== ['and', 'if', 'in', 'is', 'not', 'or']
# Multiple points after an int literal basically mean that there's a float
# and a call after that.
_check_number(Script, '1..')
_check_number(Script, '1.0.')
# power notation
_check_number(Script, '1.e14.')
_check_number(Script, '1.e-3.')
_check_number(Script, '9e3.')
assert Script('1.e3..').complete() == []
assert Script('1.e-13..').complete() == []
def test_completion_on_hex_literals(Script):
assert Script('0x1..').complete() == []
_check_number(Script, '0x1.', 'int') # hexdecimal
# Completing binary literals doesn't work if they are not actually binary
# (invalid statements).
assert Script('0b2.b').complete() == []
_check_number(Script, '0b1.', 'int') # binary
_check_number(Script, '0x2e.', 'int')
_check_number(Script, '0xE7.', 'int')
_check_number(Script, '0xEa.', 'int')
# theoretically, but people can just check for syntax errors:
assert Script('0x.').complete() == []
def test_completion_on_complex_literals(Script):
assert Script('1j..').complete() == []
_check_number(Script, '1j.', 'complex')
_check_number(Script, '44.j.', 'complex')
_check_number(Script, '4.0j.', 'complex')
# No dot no completion - I thought, but 4j is actually a literal after
# which a keyword like or is allowed. Good times, haha!
# However this has been disabled again, because it apparently annoyed
# users. So no completion after j without a space :)
assert not Script('4j').complete()
assert ({c.name for c in Script('4j ').complete()} ==
{'if', 'and', 'in', 'is', 'not', 'or'})
def test_goto_non_name(Script, environment):
assert Script('for').goto() == []
assert Script('assert').goto() == []
assert Script('True').goto() == []
def test_infer_on_non_name(Script):
assert Script('import x').infer(column=0) == []
def test_infer_on_generator(Script):
def_, = Script('def x(): yield 1\ny=x()\ny').infer()
assert def_.name == 'Generator'
def test_goto_definition_not_multiple(Script):
"""
There should be only one Definition result if it leads back to the same
origin (e.g. instance method)
"""
s = dedent('''\
import random
class A():
def __init__(self, a):
self.a = 3
def foo(self):
pass
if random.randint(0, 1):
a = A(2)
else:
a = A(1)
a''')
assert len(Script(s).infer()) == 1
def test_reference_description(Script):
descs = [u.description for u in Script("foo = ''; foo").get_references()]
assert set(descs) == {"foo = ''", 'foo'}
def test_get_line_code(Script):
def get_line_code(source, line=None, **kwargs):
return Script(source).complete(line=line)[0].get_line_code(**kwargs)
# On builtin
assert get_line_code('abs') == 'def abs(__n: SupportsAbs[_T]) -> _T: ...\n'
# On custom code
first_line = 'def foo():\n'
line = ' foo'
code = first_line + line
assert get_line_code(code) == first_line
# With before/after
code = code + '\nother_line'
assert get_line_code(code, line=2) == first_line
assert get_line_code(code, line=2, after=1) == first_line + line + '\n'
assert get_line_code(code, line=2, after=2, before=1) == code
# Should just be the whole thing, since there are no more lines on both
# sides.
assert get_line_code(code, line=2, after=3, before=3) == code
def test_get_line_code_on_builtin(Script, disable_typeshed):
abs_ = Script('abs').complete()[0]
assert abs_.name == 'abs'
assert abs_.get_line_code() == ''
assert abs_.line is None
def test_goto_follow_imports(Script):
code = dedent("""
import inspect
inspect.isfunction""")
definition, = Script(code).goto(column=0, follow_imports=True)
assert 'inspect.py' in definition.module_path
assert (definition.line, definition.column) == (1, 0)
definition, = Script(code).goto(follow_imports=True)
assert 'inspect.py' in definition.module_path
assert (definition.line, definition.column) > (1, 0)
code = '''def param(p): pass\nparam(1)'''
start_pos = 1, len('def param(')
script = Script(code)
definition, = script.goto(*start_pos, follow_imports=True)
assert (definition.line, definition.column) == start_pos
assert definition.name == 'p'
result, = definition.goto()
assert result.name == 'p'
result, = definition.infer()
assert result.name == 'int'
result, = result.infer()
assert result.name == 'int'
definition, = script.goto(*start_pos)
assert (definition.line, definition.column) == start_pos
d, = Script('a = 1\na').goto(follow_imports=True)
assert d.name == 'a'
def test_goto_module(Script):
def check(line, expected, follow_imports=False):
script = Script(path=path)
module, = script.goto(line=line, follow_imports=follow_imports)
assert module.module_path == expected
base_path = get_example_dir('simple_import')
path = os.path.join(base_path, '__init__.py')
check(1, os.path.join(base_path, 'module.py'))
check(1, os.path.join(base_path, 'module.py'), follow_imports=True)
check(5, os.path.join(base_path, 'module2.py'))
def test_goto_definition_cursor(Script):
s = ("class A():\n"
" def _something(self):\n"
" return\n"
" def different_line(self,\n"
" b):\n"
" return\n"
"A._something\n"
"A.different_line"
)
in_name = 2, 9
under_score = 2, 8
cls = 2, 7
should1 = 7, 10
diff_line = 4, 10
should2 = 8, 10
def get_def(pos):
return [d.description for d in Script(s).infer(*pos)]
in_name = get_def(in_name)
under_score = get_def(under_score)
should1 = get_def(should1)
should2 = get_def(should2)
diff_line = get_def(diff_line)
assert should1 == in_name
assert should1 == under_score
assert should2 == diff_line
assert get_def(cls) == []
def test_no_statement_parent(Script):
source = dedent("""
def f():
pass
class C:
pass
variable = f if random.choice([0, 1]) else C""")
defs = Script(source).infer(column=3)
defs = sorted(defs, key=lambda d: d.line)
assert [d.description for d in defs] == ['def f', 'class C']
def test_backslash_continuation_and_bracket(Script):
code = dedent(r"""
x = 0
a = \
[1, 2, 3, (x)]""")
lines = code.splitlines()
column = lines[-1].index('(')
def_, = Script(code).infer(line=len(lines), column=column)
assert def_.name == 'int'
def test_goto_follow_builtin_imports(Script):
s = Script('import sys; sys')
d, = s.goto(follow_imports=True)
assert d.in_builtin_module() is True
d, = s.goto(follow_imports=True, follow_builtin_imports=True)
assert d.in_builtin_module() is True
def test_docstrings_for_completions(Script):
for c in Script('').complete():
assert isinstance(c.docstring(), (str, unicode))
def test_fuzzy_completion(Script):
script = Script('string = "hello"\nstring.upper')
assert ['isupper',
'upper'] == [comp.name for comp in script.complete(fuzzy=True)]
def test_math_fuzzy_completion(Script, environment):
script = Script('import math\nmath.og')
expected = ['copysign', 'log', 'log10', 'log1p']
if environment.version_info.major >= 3:
expected.append('log2')
completions = script.complete(fuzzy=True)
assert expected == [comp.name for comp in completions]
for c in completions:
assert c.complete is None
def test_file_fuzzy_completion(Script):
path = os.path.join(test_dir, 'completion')
script = Script('"{}/ep08_i'.format(path))
expected = [
'pep0484_basic.py"',
'pep0484_generic_mismatches.py"',
'pep0484_generic_parameters.py"',
'pep0484_generic_passthroughs.py"',
'pep0484_typing.py"',
]
assert expected == [comp.name for comp in script.complete(fuzzy=True)]
@pytest.mark.parametrize(
'code, column', [
('"foo"', 0),
('"foo"', 3),
('"foo"', None),
('"""foo"""', 5),
('"""foo"""', 1),
('"""foo"""', 2),
]
)
def test_goto_on_string(Script, code, column):
script = Script(code)
assert not script.infer(column=column)
assert not script.goto(column=column)
def test_multi_goto(Script):
script = Script('x = 1\ny = 1.0\nx\ny')
x, = script.goto(line=3)
y, = script.goto(line=4)
assert x.line == 1
assert y.line == 2