Merge branch 'master' into refactor

This commit is contained in:
Dave Halter
2020-03-13 23:53:09 +01:00
29 changed files with 331 additions and 54 deletions

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ jedi.egg-info/
record.json
/.cache/
/.pytest_cache
/venv/

View File

@@ -55,5 +55,6 @@ Max Woerner Chase (@mwchase) <max.chase@gmail.com>
Johannes Maria Frank (@jmfrank63) <jmfrank63@gmail.com>
Shane Steinert-Threlkeld (@shanest) <ssshanest@gmail.com>
Tim Gates (@timgates42) <tim.gates@iress.com>
Lior Goldberg (@goldberglior)
Note: (@user) means a github user name.

View File

@@ -43,6 +43,7 @@ the CPython REPL you have to install it.
Jedi can currently be used with the following editors/projects:
- Vim (jedi-vim_, YouCompleteMe_, deoplete-jedi_, completor.vim_)
- `Visual Studio Code`_ (via `Python Extension <https://marketplace.visualstudio.com/items?itemName=ms-python.python>`_)
- Emacs (Jedi.el_, company-mode_, elpy_, anaconda-mode_, ycmd_)
- Sublime Text (SublimeJEDI_ [ST2 + ST3], anaconda_ [only ST3])
- TextMate_ (Not sure if it's actually working)
@@ -50,7 +51,6 @@ Jedi can currently be used with the following editors/projects:
<https://projects.kde.org/projects/kde/applications/kate/repository/show?rev=KDE%2F4.13>`_]
- Atom_ (autocomplete-python-jedi_)
- `GNOME Builder`_ (with support for GObject Introspection)
- `Visual Studio Code`_ (via `Python Extension <https://marketplace.visualstudio.com/items?itemName=ms-python.python>`_)
- Gedit (gedi_)
- wdb_ - Web Debugger
- `Eric IDE`_ (Available as a plugin)

View File

@@ -23,6 +23,10 @@ Vim:
- YouCompleteMe_
- deoplete-jedi_
Visual Studio Code:
- `Python Extension`_
Emacs:
- Jedi.el_
@@ -48,10 +52,6 @@ Kate:
<https://projects.kde.org/projects/kde/applications/kate/repository/entry/addons/kate/pate/src/plugins/python_autocomplete_jedi.py?rev=KDE%2F4.13>`__,
you have to enable it, though.
Visual Studio Code:
- `Python Extension`_
Atom:
- autocomplete-python-jedi_
@@ -114,4 +114,4 @@ Using a custom ``$HOME/.pythonrc.py``
.. _GNOME Builder: https://wiki.gnome.org/Apps/Builder/
.. _gedi: https://github.com/isamert/gedi
.. _Eric IDE: https://eric-ide.python-projects.org
.. _Python Extension: https://marketplace.visualstudio.com/items?itemName=donjayamanne.python
.. _Python Extension: https://marketplace.visualstudio.com/items?itemName=ms-python.python

View File

@@ -40,7 +40,7 @@ from jedi.api import Script, Interpreter, set_debug_function, \
from jedi import settings
from jedi.api.environment import find_virtualenvs, find_system_environments, \
get_default_environment, InvalidPythonEnvironment, create_environment, \
get_system_environment
get_system_environment, InterpreterEnvironment
from jedi.api.project import Project, get_default_project
from jedi.api.exceptions import InternalError, RefactoringError
# Finally load the internal plugins. This is only internal.

View File

@@ -335,7 +335,8 @@ class Script(object):
)
defs = [classes.Definition(self._inference_state, d) for d in set(names)]
return helpers.sorted_definitions(defs)
# Avoid duplicates
return list(set(helpers.sorted_definitions(defs)))
@no_py2_support
def search(self, string, **kwargs):

View File

@@ -17,6 +17,8 @@ def complete_file_name(inference_state, module_context, start_leaf, quote, strin
like_name_length = len(os.path.basename(string))
addition = _get_string_additions(module_context, start_leaf)
if string.startswith('~'):
string = os.path.expanduser(string)
if addition is None:
return
string = addition + string

View File

@@ -258,6 +258,7 @@ class Value(HelperValueMixin, BaseValue):
def _as_context(self):
raise NotImplementedError('Not all values need to be converted to contexts: %s', self)
@property
def name(self):
raise NotImplementedError

View File

@@ -484,6 +484,11 @@ class DirectObjectAccess(object):
def needs_type_completions(self):
return inspect.isclass(self._obj) and self._obj != type
def _annotation_to_str(self, annotation):
if isinstance(annotation, type):
return str(annotation.__name__)
return str(annotation)
def get_signature_params(self):
return [
SignatureParam(
@@ -493,7 +498,7 @@ class DirectObjectAccess(object):
default_string=repr(p.default),
has_annotation=p.annotation is not p.empty,
annotation=self._create_access_path(p.annotation),
annotation_string=str(p.annotation),
annotation_string=self._annotation_to_str(p.annotation),
kind_name=str(p.kind)
) for p in self._get_signature().parameters.values()
]

View File

@@ -6,6 +6,7 @@ information returned to enable Jedi to make decisions.
import types
from jedi import debug
from jedi._compatibility import py_version
_sentinel = object()
@@ -54,7 +55,14 @@ def _shadowed_dict_newstyle(klass):
def _static_getmro_newstyle(klass):
return type.__dict__['__mro__'].__get__(klass)
mro = type.__dict__['__mro__'].__get__(klass)
if not isinstance(mro, (tuple, list)):
# There are unfortunately no tests for this, I was not able to
# reproduce this in pure Python. However should still solve the issue
# raised in GH #1517.
debug.warning('mro of %s returned %s, should be a tuple' % (klass, mro))
return ()
return mro
if py_version >= 30:

View File

@@ -297,7 +297,7 @@ class Listener(object):
try:
inference_state = self._inference_states[inference_state_id]
except KeyError:
from jedi.api.environment import InterpreterEnvironment
from jedi import InterpreterEnvironment
inference_state = InferenceState(
# The project is not actually needed. Nothing should need to
# access it.

View File

@@ -130,6 +130,9 @@ def import_module_decorator(func):
def try_to_load_stub_cached(inference_state, import_names, *args, **kwargs):
if import_names is None:
return None
try:
return inference_state.stub_module_cache[import_names]
except KeyError:

View File

@@ -5,8 +5,11 @@ values.
This file deals with all the typing.py cases.
"""
import itertools
from jedi._compatibility import unicode
from jedi import debug
from jedi.inference.compiled import builtin_from_name
from jedi.inference.compiled import builtin_from_name, create_simple_object
from jedi.inference.base_value import ValueSet, NO_VALUES, Value, \
LazyValueWrapper
from jedi.inference.lazy_value import LazyKnownValues
@@ -81,7 +84,8 @@ class TypingModuleName(NameWrapper):
elif name == 'TypedDict':
# TODO doesn't even exist in typeshed/typing.py, yet. But will be
# added soon.
pass
yield TypedDictBase.create_cached(
inference_state, self.parent_context, self.tree_name)
elif name in ('no_type_check', 'no_type_check_decorator'):
# This is not necessary, as long as we are not doing type checking.
for c in self._wrapped_name.infer(): # Fuck my life Python 2
@@ -339,3 +343,47 @@ class CastFunction(BaseTypingValue):
@repack_with_argument_clinic('type, object, /')
def py__call__(self, type_value_set, object_value_set):
return type_value_set.execute_annotation()
class TypedDictBase(BaseTypingValue):
"""
This class has no responsibilities and is just here to make sure that typed
dicts can be identified.
"""
class TypedDict(LazyValueWrapper):
"""Represents the instance version of ``TypedDictClass``."""
def __init__(self, definition_class):
self.inference_state = definition_class.inference_state
self.parent_context = definition_class.parent_context
self.tree_node = definition_class.tree_node
self._definition_class = definition_class
@property
def name(self):
return ValueName(self, self.tree_node.name)
def py__simple_getitem__(self, index):
if isinstance(index, unicode):
return ValueSet.from_sets(
name.infer()
for filter in self._definition_class.get_filters(is_instance=True)
for name in filter.get(index)
)
return NO_VALUES
def get_key_values(self):
filtered_values = itertools.chain.from_iterable((
f.values()
for f in self._definition_class.get_filters(is_instance=True)
))
return ValueSet({
create_simple_object(self.inference_state, v.string_name)
for v in filtered_values
})
def _get_wrapped_value(self):
d, = self.inference_state.builtins_module.py__getattribute__('dict')
result, = d.execute_with_values()
return result

View File

@@ -51,6 +51,25 @@ class ExecutedParamName(ParamName):
def get_executed_param_names_and_issues(function_value, arguments):
"""
Return a tuple of:
- a list of `ExecutedParamName`s corresponding to the arguments of the
function execution `function_value`, containing the inferred value of
those arguments (whether explicit or default)
- a list of the issues encountered while building that list
For example, given:
```
def foo(a, b, c=None, d='d'): ...
foo(42, c='c')
```
Then for the execution of `foo`, this will return a tuple containing:
- a list with entries for each parameter a, b, c & d; the entries for a,
c, & d will have their values (42, 'c' and 'd' respectively) included.
- a list with a single entry about the lack of a value for `b`
"""
def too_many_args(argument):
m = _error_argument_count(funcdef, len(unpacked_va))
# Just report an error for the first param that is not needed (like
@@ -207,6 +226,23 @@ def get_executed_param_names_and_issues(function_value, arguments):
def get_executed_param_names(function_value, arguments):
"""
Return a list of `ExecutedParamName`s corresponding to the arguments of the
function execution `function_value`, containing the inferred value of those
arguments (whether explicit or default). Any issues building this list (for
example required arguments which are missing in the invocation) are ignored.
For example, given:
```
def foo(a, b, c=None, d='d'): ...
foo(42, c='c')
```
Then for the execution of `foo`, this will return a list containing entries
for each parameter a, b, c & d; the entries for a, c, & d will have their
values (42, 'c' and 'd' respectively) included.
"""
return get_executed_param_names_and_issues(function_value, arguments)[0]

View File

@@ -356,6 +356,12 @@ def infer_atom(context, atom):
def infer_expr_stmt(context, stmt, seek_name=None):
with recursion.execution_allowed(context.inference_state, stmt) as allowed:
if allowed:
if seek_name is not None:
pep0484_values = \
annotation.find_type_from_comment_hint_assign(context, stmt, seek_name)
if pep0484_values:
return pep0484_values
return _infer_expr_stmt(context, stmt, seek_name)
return NO_VALUES
@@ -632,23 +638,6 @@ def _infer_comparison_part(inference_state, context, left, operator, right):
return result
def _remove_statements(context, stmt, name):
"""
This is the part where statements are being stripped.
Due to lazy type inference, statements like a = func; b = a; b() have to be
inferred.
TODO merge with infer_expr_stmt?
"""
pep0484_values = \
annotation.find_type_from_comment_hint_assign(context, stmt, name)
if pep0484_values:
return pep0484_values
return infer_expr_stmt(context, stmt, seek_name=name)
@plugin_manager.decorate()
def tree_name_to_values(inference_state, context, tree_name):
value_set = NO_VALUES
@@ -713,7 +702,7 @@ def tree_name_to_values(inference_state, context, tree_name):
n = TreeNameDefinition(context, tree_name)
types = check_tuple_assignments(n, for_types)
elif typ == 'expr_stmt':
types = _remove_statements(context, node, tree_name)
types = infer_expr_stmt(context, node, tree_name)
elif typ == 'with_stmt':
value_managers = context.infer_node(node.get_test_node_from_name(tree_name))
enter_methods = value_managers.py__getattribute__(u'__enter__')

View File

@@ -547,10 +547,10 @@ class InstanceClassFilter(AbstractFilter):
self._class_filter = class_filter
def get(self, name):
return self._convert(self._class_filter.get(name, from_instance=True))
return self._convert(self._class_filter.get(name))
def values(self):
return self._convert(self._class_filter.values(from_instance=True))
return self._convert(self._class_filter.values())
def _convert(self, names):
return [
@@ -586,7 +586,7 @@ class SelfAttributeFilter(ClassFilter):
if trailer.type == 'trailer' \
and len(trailer.parent.children) == 2 \
and trailer.children[0] == '.':
if name.is_definition() and self._access_possible(name, from_instance=True):
if name.is_definition() and self._access_possible(name):
# TODO filter non-self assignments instead of this bad
# filter.
if self._is_in_right_scope(trailer.parent.children[0], name):

View File

@@ -38,11 +38,11 @@ py__doc__() Returns the docstring for a value.
"""
from jedi import debug
from jedi._compatibility import use_metaclass
from jedi.parser_utils import get_cached_parent_scope
from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted
from jedi.inference.cache import inference_state_method_cache, CachedMetaClass, \
inference_state_method_generator_cache
from jedi.inference import compiled
from jedi.inference.lazy_value import LazyKnownValues
from jedi.inference.lazy_value import LazyKnownValues, LazyTreeValue
from jedi.inference.filters import ParserTreeFilter
from jedi.inference.names import TreeNameDefinition, ValueName
from jedi.inference.arguments import unpack_arglist, ValuesArguments
@@ -104,27 +104,31 @@ class ClassFilter(ParserTreeFilter):
node = get_cached_parent_scope(self._used_names, node)
return False
def _access_possible(self, name, from_instance=False):
def _access_possible(self, name):
# Filter for ClassVar variables
# TODO this is not properly done, yet. It just checks for the string
# ClassVar in the annotation, which can be quite imprecise. If we
# wanted to do this correct, we would have to infer the ClassVar.
if not from_instance:
if not self._is_instance:
expr_stmt = name.get_definition()
if expr_stmt is not None and expr_stmt.type == 'expr_stmt':
annassign = expr_stmt.children[1]
if annassign.type == 'annassign':
# TODO this is not proper matching
if 'ClassVar' not in annassign.children[1].get_code():
# If there is an =, the variable is obviously also
# defined on the class.
if 'ClassVar' not in annassign.children[1].get_code() \
and '=' not in annassign.children:
return False
# Filter for name mangling of private variables like __foo
return not name.value.startswith('__') or name.value.endswith('__') \
or self._equals_origin_scope()
def _filter(self, names, from_instance=False):
def _filter(self, names):
names = super(ClassFilter, self)._filter(names)
return [name for name in names if self._access_possible(name, from_instance)]
return [name for name in names if self._access_possible(name)]
class ClassMixin(object):
@@ -133,6 +137,10 @@ class ClassMixin(object):
def py__call__(self, arguments=None):
from jedi.inference.value import TreeInstance
from jedi.inference.gradual.typing import TypedDict
if self.is_typeddict():
return ValueSet([TypedDict(self)])
return ValueSet([TreeInstance(self.inference_state, self.parent_context, self, arguments)])
def py__class__(self):
@@ -226,6 +234,36 @@ class ClassMixin(object):
return 'Type[%s]' % self.py__name__()
return self.py__name__()
@inference_state_method_cache(default=False)
def is_typeddict(self):
# TODO Do a proper mro resolution. Currently we are just listing
# classes. However, it's a complicated algorithm.
from jedi.inference.gradual.typing import TypedDictBase
for lazy_cls in self.py__bases__():
if not isinstance(lazy_cls, LazyTreeValue):
return False
tree_node = lazy_cls.data
# Only resolve simple classes, stuff like Iterable[str] are more
# intensive to resolve and if generics are involved, we know it's
# not a TypedDict.
if not expr_is_dotted(tree_node):
return False
for cls in lazy_cls.infer():
if isinstance(cls, TypedDictBase):
return True
try:
method = cls.is_typeddict
except AttributeError:
# We're only dealing with simple classes, so just returning
# here should be fine. This only happens with e.g. compiled
# classes.
return False
else:
if method():
return True
return False
class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase)):
api_type = u'class'

View File

@@ -293,6 +293,25 @@ def cut_value_at_position(leaf, position):
return ''.join(lines)
def expr_is_dotted(node):
"""
Checks if a path looks like `name` or `name.foo.bar` and not `name()`.
"""
if node.type == 'atom':
if len(node.children) == 3 and node.children[0] == '(':
return expr_is_dotted(node.children[1])
return False
if node.type == 'atom_expr':
children = node.children
if children[0] == 'await':
return False
if not expr_is_dotted(children[0]):
return False
# Check trailers
return all(c.children[0] == '.' for c in children[1:])
return node.type == 'name'
def _function_is_x_method(method_name):
def wrapper(function_node):
"""

View File

@@ -44,8 +44,6 @@ from operator import itemgetter as _itemgetter
from collections import OrderedDict
class {typename}(tuple):
'{typename}({arg_list})'
__slots__ = ()
_fields = {field_names!r}

10
sith.py
View File

@@ -20,8 +20,7 @@ Run a specific operation
./sith.py run <operation> </path/to/source/file.py> <line> <col>
Where operation is one of completions, goto_assignments, goto_definitions,
usages, or call_signatures.
Where operation is one of complete, goto, infer, get_references or get_signatures.
Note: Line numbers start at 1; columns start at 0 (this is consistent with
many text editors, including Emacs).
@@ -95,6 +94,7 @@ class TestCase(object):
args = json.load(f)
return cls(*args)
# Changing this? Also update the module docstring above.
operations = ['complete', 'goto', 'infer', 'get_references', 'get_signatures']
@classmethod
@@ -151,13 +151,13 @@ class TestCase(object):
# Three lines ought to be enough
lower = lineno - show if lineno - show > 0 else 0
prefix = ' |'
for i, line in enumerate(self.script._source.split('\n')[lower:lineno]):
for i, line in enumerate(self.script._code.split('\n')[lower:lineno]):
print(prefix, lower + i + 1, line)
print(prefix, ' ', ' ' * (column + len(str(lineno))), '^')
print(prefix, ' ' * (column + len(str(lineno))), '^')
def show_operation(self):
print("%s:\n" % self.operation.capitalize())
if self.operation == 'completions':
if self.operation == 'complete':
self.show_completions()
else:
self.show_definitions()

View File

@@ -422,3 +422,11 @@ with Foo() as f3:
#? 6 Foo
with Foo() as f3:
f3
# -----------------
# Avoiding multiple definitions
# -----------------
some_array = ['', '']
#! ['def upper']
some_array[some_not_defined_index].upper

View File

@@ -499,3 +499,89 @@ def dynamic_annotation(x: int):
#? int()
dynamic_annotation('')
# -------------------------
# TypeDict
# -------------------------
# python >= 3.8
class Foo(typing.TypedDict):
foo: str
bar: typing.List[float]
an_int: int
#! ['foo: str']
foo
#? str()
foo
#? int()
an_int
def typed_dict_test_foo(arg: Foo):
a_string = arg['foo']
a_list_of_floats = arg['bar']
an_int = arg['an_int']
#? str()
a_string
#? list()
a_list_of_floats
#? float()
a_list_of_floats[0]
#? int()
an_int
#? ['isupper']
a_string.isuppe
#? ['pop']
a_list_of_floats.po
#? ['as_integer_ratio']
an_int.as_integer_rati
#! ['class Foo']
d: Foo
#? str()
d['foo']
#? float()
d['bar'][0]
#?
d['baz']
#?
d.foo
#?
d.bar
#! []
d.foo
#? []
Foo.set
#? ['setdefault']
d.setdefaul
#? []
Foo.setdefaul
#? 5 ["'foo"]
d['fo']
#? 5 ['"bar"']
d["bar"]
class Bar(Foo):
another_variable: int
#? int()
another_variable
#?
an_int
def typed_dict_test_foo(arg: Bar):
#? str()
arg['foo']
#? list()
arg['bar']
#? float()
arg['bar'][0]
#? int()
arg['an_int']
#? int()
arg['another_variable']

View File

@@ -57,7 +57,7 @@ Foo.baz
Foo().baz
class VarClass:
var_instance1: int = 1
var_instance1: int = ''
var_instance2: float
var_class1: typing.ClassVar[str] = 1
var_class2: typing.ClassVar[bytes]
@@ -77,9 +77,9 @@ class VarClass:
self.var_
#? ['var_class1', 'var_class2']
#? ['var_class1', 'var_class2', 'var_instance1']
VarClass.var_
#?
#? int()
VarClass.var_instance1
#?
VarClass.var_instance2

View File

@@ -91,3 +91,15 @@ class B:
for i in self.a(i):
#?
yield i
foo = int
foo = foo # type: foo
#? int
foo
while True:
bar = int
bar = bar # type: bar
#? int()
bar

View File

@@ -8,7 +8,7 @@ import pytest
from . import helpers
from . import run
from . import refactor
from jedi.api.environment import InterpreterEnvironment, get_system_environment
from jedi import InterpreterEnvironment, get_system_environment
from jedi.inference.compiled.value import create_from_access_path
from jedi.inference.imports import _load_python_module
from jedi.file_io import KnownContentFileIO

View File

@@ -1,4 +1,4 @@
from os.path import join, sep as s, dirname
from os.path import join, sep as s, dirname, expanduser
import os
import sys
from textwrap import dedent
@@ -7,6 +7,7 @@ import pytest
from ..helpers import root_dir
from jedi.api.helpers import _start_match, _fuzzy_match
from jedi._compatibility import scandir
def test_in_whitespace(Script):
@@ -86,6 +87,18 @@ def test_loading_unicode_files_with_bad_global_charset(Script, monkeypatch, tmpd
s.complete(line=2, column=4)
def test_complete_expanduser(Script):
possibilities = scandir(expanduser('~'))
non_dots = [p for p in possibilities if not p.name.startswith('.') and len(p.name) > 1]
item = non_dots[0]
line = "'~%s%s'" % (os.sep, item.name)
s = Script(line, line=1, column=len(line)-1)
expected_name = item.name
if item.is_dir():
expected_name += os.path.sep
assert expected_name in [c.name for c in s.completions()]
def test_fake_subnodes(Script):
"""
Test the number of subnodes of a fake object.

View File

@@ -342,7 +342,7 @@ def test_completion_params():
@pytest.mark.skipif('py_version < 33', reason='inspect.signature was created in 3.3.')
def test_completion_param_annotations():
# Need to define this function not directly in Python. Otherwise Jedi is to
# Need to define this function not directly in Python. Otherwise Jedi is too
# clever and uses the Python code instead of the signature object.
code = 'def foo(a: 1, b: str, c: int = 1.0) -> bytes: pass'
exec_(code, locals())
@@ -354,6 +354,10 @@ def test_completion_param_annotations():
assert [d.name for d in b.infer()] == ['str']
assert {d.name for d in c.infer()} == {'int', 'float'}
assert a.description == 'param a: 1'
assert b.description == 'param b: str'
assert c.description == 'param c: int=1.0'
d, = jedi.Interpreter('foo()', [locals()]).infer()
assert d.name == 'bytes'

View File

@@ -77,6 +77,7 @@ def test_namedtuple_infer(Script):
assert d1.get_line_code() == "class Foo(tuple):\n"
assert d1.module_path is None
assert d1.docstring() == 'Foo(id, timestamp, gps_timestamp, attributes)'
def test_re_sub(Script, environment):

View File

@@ -41,6 +41,9 @@ def test_completion(case, monkeypatch, environment, has_typing):
if skip_reason is not None:
pytest.skip(skip_reason)
if 'pep0484_typing' in case.path and sys.version_info[0] == 2:
pytest.skip('ditch python 2 finally')
_CONTAINS_TYPING = ('pep0484_typing', 'pep0484_comments', 'pep0526_variables')
if not has_typing and any(x in case.path for x in _CONTAINS_TYPING):
pytest.skip('Needs the typing module installed to run this test.')