Merge branch 'master' into fix-nested-tuple-argument

This commit is contained in:
Peter Law
2020-04-26 13:56:14 +01:00
29 changed files with 512 additions and 57 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "jedi/third_party/typeshed"]
path = jedi/third_party/typeshed
url = https://github.com/davidhalter/typeshed.git
[submodule "jedi/third_party/django-stubs"]
path = jedi/third_party/django-stubs
url = https://github.com/typeddjango/django-stubs

View File

@@ -3,6 +3,14 @@
Changelog
---------
Unreleased: 0.17.1 (2020-04-)
+++++++++++++++++++
- Django ``Model`` meta class support
- Added Django Stubs to Jedi, thanks to all contributors of the
`Django Stubs <https://github.com/typeddjango/django-stubs>`_ project
- A few bugfixes
0.17.0 (2020-04-14)
+++++++++++++++++++

View File

@@ -11,6 +11,7 @@ include requirements.txt
include jedi/parser/python/grammar*.txt
recursive-include jedi/third_party *.pyi
include jedi/third_party/typeshed/LICENSE
include jedi/third_party/django-stubs/LICENSE.txt
include jedi/third_party/typeshed/README
recursive-include test *
recursive-include docs *

View File

@@ -13,7 +13,7 @@ from test.helpers import test_dir
collect_ignore = [
'setup.py',
'__main__.py',
'jedi/__main__.py',
'jedi/inference/compiled/subprocess/__main__.py',
'build/',
'test/examples',
@@ -147,6 +147,12 @@ def has_typing(environment):
return bool(script.infer())
@pytest.fixture(scope='session')
def has_django(environment):
script = jedi.Script('import django', environment=environment)
return bool(script.infer())
@pytest.fixture(scope='session')
def jedi_path():
return os.path.dirname(__file__)

View File

@@ -27,7 +27,7 @@ ad
load
"""
__version__ = '0.17.0'
__version__ = '0.17.1'
from jedi.api import Script, Interpreter, set_debug_function, \
preload_module, names

View File

@@ -44,20 +44,29 @@ def _complete():
import jedi
import pdb
if '-d' in sys.argv:
sys.argv.remove('-d')
jedi.set_debug_function()
try:
for c in jedi.Script(sys.argv[2]).complete():
completions = jedi.Script(sys.argv[2]).complete()
for c in completions:
c.docstring()
c.type
except Exception as e:
print(e)
print(repr(e))
pdb.post_mortem()
else:
print(completions)
if len(sys.argv) == 2 and sys.argv[1] == 'repl':
# don't want to use __main__ only for repl yet, maybe we want to use it for
# something else. So just use the keyword ``repl`` for now.
print(join(dirname(abspath(__file__)), 'api', 'replstartup.py'))
elif len(sys.argv) > 1 and sys.argv[1] == 'linter':
elif len(sys.argv) > 1 and sys.argv[1] == '_linter':
_start_linter()
elif len(sys.argv) > 1 and sys.argv[1] == '_complete':
_complete()
else:
print('Command not implemented: %s' % sys.argv[1])

View File

@@ -339,6 +339,13 @@ try:
except NameError:
PermissionError = IOError
try:
NotADirectoryError = NotADirectoryError
except NameError:
class NotADirectoryError(Exception):
# Don't implement this for Python 2 anymore.
pass
def no_unicode_pprint(dct):
"""

View File

@@ -472,9 +472,20 @@ class Script(object):
if definitions:
return definitions
leaf = self._module_node.get_leaf_for_position((line, column))
if leaf.type in ('keyword', 'operator', 'error_leaf'):
reserved = self._inference_state.grammar._pgen_grammar.reserved_syntax_strings.keys()
if leaf.value in reserved:
if leaf is not None and leaf.type in ('keyword', 'operator', 'error_leaf'):
def need_pydoc():
if leaf.value in ('(', ')', '[', ']'):
if leaf.parent.type == 'trailer':
return False
if leaf.parent.type == 'atom':
return False
grammar = self._inference_state.grammar
# This parso stuff is not public, but since I control it, this
# is fine :-) ~dave
reserved = grammar._pgen_grammar.reserved_syntax_strings.keys()
return leaf.value in reserved
if need_pydoc():
name = KeywordName(self._inference_state, leaf.value)
return [classes.Name(self._inference_state, name)]
return []

View File

@@ -94,7 +94,11 @@ class BaseName(object):
@property
def module_path(self):
"""Shows the file path of a module. e.g. ``/usr/lib/python2.7/os.py``"""
"""
Shows the file path of a module. e.g. ``/usr/lib/python2.7/os.py``
:rtype: str or None
"""
module = self._get_module_context()
if module.is_stub() or not module.is_compiled():
# Compiled modules should not return a module path even if they
@@ -168,7 +172,7 @@ class BaseName(object):
>>> defs[3]
'function'
Valid values for are ``module``, ``class``, ``instance``, ``function``,
Valid values for type are ``module``, ``class``, ``instance``, ``function``,
``param``, ``path``, ``keyword`` and ``statement``.
"""
@@ -245,8 +249,8 @@ class BaseName(object):
Document for function f.
Notice that useful extra information is added to the actual
docstring. For function, it is signature. If you need
actual docstring, use ``raw=True`` instead.
docstring, e.g. function signatures are prepended to their docstrings.
If you need the actual docstring, use ``raw=True`` instead.
>>> print(script.infer(1, len('def f'))[0].docstring(raw=True))
Document for function f.
@@ -665,7 +669,7 @@ class Completion(BaseName):
def docstring(self, raw=False, fast=True):
"""
Documentated under :meth:`BaseName.docstring`.
Documented under :meth:`BaseName.docstring`.
"""
if self._like_name_length >= 3:
# In this case we can just resolve the like name, because we
@@ -703,7 +707,7 @@ class Completion(BaseName):
@property
def type(self):
"""
Documentated under :meth:`BaseName.type`.
Documented under :meth:`BaseName.type`.
"""
# Purely a speed optimization.
if self._cached_name is not None:
@@ -734,8 +738,7 @@ class Name(BaseName):
DeprecationWarning,
stacklevel=2
)
position = '' if self.in_builtin_module else '@%s' % self.line
return "%s:%s%s" % (self.module_name, self.description, position)
return "%s:%s" % (self.module_name, self.description)
@memoize_method
def defined_names(self):
@@ -798,7 +801,7 @@ class BaseSignature(Name):
Returns a text representation of the signature. This could for example
look like ``foo(bar, baz: int, **kwargs)``.
:return str
:rtype: str
"""
return self._signature.to_string()
@@ -865,7 +868,7 @@ class ParamName(Name):
Returns a simple representation of a param, like
``f: Callable[..., Any]``.
:rtype: :class:`str`
:rtype: str
"""
return self._name.to_string()

View File

@@ -13,7 +13,7 @@ import json
import sys
from jedi._compatibility import FileNotFoundError, PermissionError, \
IsADirectoryError
IsADirectoryError, NotADirectoryError
from jedi import debug
from jedi.api.environment import get_cached_default_environment, create_environment
from jedi.api.exceptions import WrongVersion
@@ -383,6 +383,8 @@ def get_default_project(path=None):
return Project.load(dir)
except (FileNotFoundError, IsADirectoryError, PermissionError):
pass
except NotADirectoryError:
continue
if first_no_init_file is None:
if os.path.exists(os.path.join(dir, '__init__.py')):

View File

@@ -99,7 +99,7 @@ class DefineGenericBase(LazyValueWrapper):
for generic_set in self.get_generics():
values = NO_VALUES
for generic in generic_set:
if isinstance(generic, (GenericClass, TypeVar)):
if isinstance(generic, (DefineGenericBase, TypeVar)):
result = generic.define_generics(type_var_dict)
values |= result
if result != ValueSet({generic}):

View File

@@ -12,6 +12,8 @@ from jedi.inference.value import ModuleValue
_jedi_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TYPESHED_PATH = os.path.join(_jedi_path, 'third_party', 'typeshed')
DJANGO_INIT_PATH = os.path.join(_jedi_path, 'third_party', 'django-stubs',
'django-stubs', '__init__.pyi')
_IMPORT_MAP = dict(
_collections='collections',
@@ -173,6 +175,13 @@ def _try_to_load_stub(inference_state, import_names, python_value_set,
)
if m is not None:
return m
if import_names[0] == 'django':
return _try_to_load_stub_from_file(
inference_state,
python_value_set,
file_io=FileIO(DJANGO_INIT_PATH),
import_names=import_names,
)
# 2. Try to load pyi files next to py files.
for c in python_value_set:

View File

@@ -226,6 +226,21 @@ class TypingClassValueWithIndex(_TypingClassMixin, TypingValueWithIndex):
elif annotation_name == 'Callable':
if len(annotation_generics) == 2:
if is_class_value:
# This only applies if we are comparing something like
# List[Callable[..., T]] with Iterable[Callable[..., T]].
# First, Jedi tries to match List/Iterable. After that we
# will land here, because is_class_value will be True at
# that point. Obviously we also compare below that both
# sides are `Callable`.
for element in value_set:
element_name = element.py__name__()
if element_name == 'Callable':
merge_type_var_dicts(
type_var_dict,
merge_pairwise_generics(self, element),
)
else:
return annotation_generics[1].infer_type_vars(
value_set.execute_annotation(),
)
@@ -421,6 +436,10 @@ class NewType(Value):
self._type_value_set = type_value_set
self.tree_node = tree_node
def py__class__(self):
c, = self._type_value_set.py__class__()
return c
def py__call__(self, arguments):
return self._type_value_set.execute_annotation()

View File

@@ -192,13 +192,17 @@ class Sequence(LazyAttributeOverwrite, IterableMixin):
def _get_generics(self):
return (self.merge_types_of_iterate().py__class__(),)
@inference_state_method_cache(default=())
def _cached_generics(self):
return self._get_generics()
def _get_wrapped_value(self):
from jedi.inference.gradual.base import GenericClass
from jedi.inference.gradual.generics import TupleGenericManager
klass = compiled.builtin_from_name(self.inference_state, self.array_type)
c, = GenericClass(
klass,
TupleGenericManager(self._get_generics())
TupleGenericManager(self._cached_generics())
).execute_annotation()
return c

View File

@@ -186,7 +186,8 @@ class ClassMixin(object):
mro.append(cls_new)
yield cls_new
def get_filters(self, origin_scope=None, is_instance=False):
def get_filters(self, origin_scope=None, is_instance=False, include_metaclasses=True):
if include_metaclasses:
metaclasses = self.get_metaclasses()
if metaclasses:
for f in self.get_metaclass_filters(metaclasses):

130
jedi/plugins/django.py Normal file
View File

@@ -0,0 +1,130 @@
"""
Module is used to infer Django model fields.
"""
from jedi import debug
from jedi.inference.base_value import ValueSet, iterator_to_value_set
from jedi.inference.filters import ParserTreeFilter, DictFilter
from jedi.inference.names import NameWrapper
from jedi.inference.value.instance import TreeInstance
from jedi.inference.gradual.base import GenericClass
from jedi.inference.gradual.generics import TupleGenericManager
mapping = {
'IntegerField': (None, 'int'),
'BigIntegerField': (None, 'int'),
'PositiveIntegerField': (None, 'int'),
'SmallIntegerField': (None, 'int'),
'CharField': (None, 'str'),
'TextField': (None, 'str'),
'EmailField': (None, 'str'),
'FloatField': (None, 'float'),
'BinaryField': (None, 'bytes'),
'BooleanField': (None, 'bool'),
'DecimalField': ('decimal', 'Decimal'),
'TimeField': ('datetime', 'time'),
'DurationField': ('datetime', 'timedelta'),
'DateField': ('datetime', 'date'),
'DateTimeField': ('datetime', 'datetime'),
}
def _infer_scalar_field(inference_state, field_name, field_tree_instance):
try:
module_name, attribute_name = mapping[field_tree_instance.py__name__()]
except KeyError:
return None
if module_name is None:
module = inference_state.builtins_module
else:
module = inference_state.import_module((module_name,))
for attribute in module.py__getattribute__(attribute_name):
return attribute.execute_with_values()
@iterator_to_value_set
def _get_foreign_key_values(cls, field_tree_instance):
if isinstance(field_tree_instance, TreeInstance):
# TODO private access..
argument_iterator = field_tree_instance._arguments.unpack()
key, lazy_values = next(argument_iterator, (None, None))
if key is None and lazy_values is not None:
for value in lazy_values.infer():
if value.py__name__() == 'str':
foreign_key_class_name = value.get_safe_value()
module = cls.get_root_context()
for v in module.py__getattribute__(foreign_key_class_name):
if v.is_class():
yield v
elif value.is_class():
yield value
def _infer_field(cls, field_name):
inference_state = cls.inference_state
for field_tree_instance in field_name.infer():
scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance)
if scalar_field is not None:
return scalar_field
name = field_tree_instance.py__name__()
is_many_to_many = name == 'ManyToManyField'
if name == 'ForeignKey' or is_many_to_many:
values = _get_foreign_key_values(cls, field_tree_instance)
if is_many_to_many:
return ValueSet(filter(None, [
_create_manager_for(v, 'RelatedManager') for v in values
]))
else:
return values.execute_with_values()
debug.dbg('django plugin: fail to infer `%s` from class `%s`',
field_name.string_name, cls.py__name__())
return field_name.infer()
class DjangoModelName(NameWrapper):
def __init__(self, cls, name):
super(DjangoModelName, self).__init__(name)
self._cls = cls
def infer(self):
return _infer_field(self._cls, self._wrapped_name)
def _create_manager_for(cls, manager_cls='BaseManager'):
managers = cls.inference_state.import_module(
('django', 'db', 'models', 'manager')
).py__getattribute__(manager_cls)
for m in managers:
if m.is_class() and not m.is_compiled():
generics_manager = TupleGenericManager((ValueSet([cls]),))
for c in GenericClass(m, generics_manager).execute_annotation():
return c
return None
def _new_dict_filter(cls):
filters = cls.get_filters(is_instance=True, include_metaclasses=False)
dct = {
name.string_name: DjangoModelName(cls, name)
for filter_ in reversed(list(filters))
for name in filter_.values()
}
manager = _create_manager_for(cls)
if manager:
dct['objects'] = manager.name
return DictFilter(dct)
def get_metaclass_filters(func):
def wrapper(cls, metaclasses):
for metaclass in metaclasses:
if metaclass.py__name__() == 'ModelBase' \
and metaclass.get_root_context().py__name__() == 'django.db.models.base':
return [_new_dict_filter(cls)]
return func(cls, metaclasses)
return wrapper

View File

@@ -5,7 +5,8 @@ This is not a plugin, this is just the place were plugins are registered.
from jedi.plugins import stdlib
from jedi.plugins import flask
from jedi.plugins import pytest
from jedi.plugins import django
from jedi.plugins import plugin_manager
plugin_manager.register(stdlib, flask, pytest)
plugin_manager.register(stdlib, flask, pytest, django)

View File

@@ -19,6 +19,8 @@ with open('requirements.txt') as f:
assert os.path.isfile("jedi/third_party/typeshed/LICENSE"), \
"Please download the typeshed submodule first (Hint: git submodule update --init)"
assert os.path.isfile("jedi/third_party/django-stubs/LICENSE.txt"), \
"Please download the django-stubs submodule first (Hint: git submodule update --init)"
setup(name='jedi',
version=version,
@@ -43,6 +45,7 @@ setup(name='jedi',
'docopt',
# coloroma for colored debug output
'colorama',
'Django<3.1', # For now pin this.
],
'qa': [
'flake8==3.7.9',

View File

@@ -123,7 +123,7 @@ class TestCase(object):
with open(self.path) as f:
self.script = jedi.Script(f.read(), path=self.path)
kwargs = {}
if self.operation == 'goto_assignments':
if self.operation == 'goto':
kwargs['follow_imports'] = random.choice([False, True])
self.objects = getattr(self.script, self.operation)(self.line, self.column, **kwargs)

168
test/completion/django.py Normal file
View File

@@ -0,0 +1,168 @@
import datetime
import decimal
from django.db import models
from django.contrib.auth.models import User
class Tag(models.Model):
tag_name = models.CharField()
class Category(models.Model):
category_name = models.CharField()
class BusinessModel(models.Model):
category_fk = models.ForeignKey(Category)
category_fk2 = models.ForeignKey('Category')
category_fk3 = models.ForeignKey(1)
category_fk4 = models.ForeignKey('models')
category_fk5 = models.ForeignKey()
integer_field = models.IntegerField()
big_integer_field = models.BigIntegerField()
positive_integer_field = models.PositiveIntegerField()
small_integer_field = models.SmallIntegerField()
char_field = models.CharField()
text_field = models.TextField()
email_field = models.EmailField()
float_field = models.FloatField()
binary_field = models.BinaryField()
boolean_field = models.BooleanField()
decimal_field = models.DecimalField()
time_field = models.TimeField()
duration_field = models.DurationField()
date_field = models.DateField()
date_time_field = models.DateTimeField()
tags_m2m = models.ManyToManyField(Tag)
unidentifiable = NOT_FOUND
# -----------------
# Model attribute inference
# -----------------
model_instance = BusinessModel()
#? int()
model_instance.integer_field
#? int()
model_instance.big_integer_field
#? int()
model_instance.positive_integer_field
#? int()
model_instance.small_integer_field
#? str()
model_instance.char_field
#? str()
model_instance.text_field
#? str()
model_instance.email_field
#? float()
model_instance.float_field
#? bytes()
model_instance.binary_field
#? bool()
model_instance.boolean_field
#? decimal.Decimal()
model_instance.decimal_field
#? datetime.time()
model_instance.time_field
#? datetime.timedelta()
model_instance.duration_field
#? datetime.date()
model_instance.date_field
#? datetime.datetime()
model_instance.date_time_field
#! ['category_fk = models.ForeignKey(Category)']
model_instance.category_fk
#! ['category_name = models.CharField()']
model_instance.category_fk.category_name
#? Category()
model_instance.category_fk
#? str()
model_instance.category_fk.category_name
#? Category()
model_instance.category_fk2
#? str()
model_instance.category_fk2.category_name
#?
model_instance.category_fk3
#?
model_instance.category_fk4
#?
model_instance.category_fk5
#? models.manager.RelatedManager()
model_instance.tags_m2m
#? Tag()
model_instance.tags_m2m.get()
#? ['add']
model_instance.tags_m2m.add
#?
model_instance.unidentifiable
#! ['unidentifiable = NOT_FOUND']
model_instance.unidentifiable
# -----------------
# Queries
# -----------------
#? models.query.QuerySet.filter
model_instance.objects.filter
#? BusinessModel() None
model_instance.objects.filter().first()
#? str()
model_instance.objects.get().char_field
#? int()
model_instance.objects.update(x='')
#? BusinessModel()
model_instance.objects.create()
# -----------------
# Inheritance
# -----------------
class Inherited(BusinessModel):
text_field = models.IntegerField()
new_field = models.FloatField()
inherited = Inherited()
#? int()
inherited.text_field
#? str()
inherited.char_field
#? float()
inherited.new_field
#? str()
inherited.category_fk2.category_name
#? str()
inherited.objects.get().char_field
#? int()
inherited.objects.get().text_field
#? float()
inherited.objects.get().new_field
# -----------------
# Django Auth
# -----------------
#? str()
User().email
#? str()
User.objects.get().email
# -----------------
# values & values_list (dave is too lazy to implement it)
# -----------------
#?
model_instance.objects.values_list('char_field')[0]
#? dict()
model_instance.objects.values('char_field')[0]
#?
model_instance.objects.values('char_field')[0]['char_field']

View File

@@ -207,40 +207,36 @@ for a in list_func_t_to_list_t(12):
a
# The following are all actually wrong, however we're mainly testing here that
# we don't error when processing invalid values, rather than that we get the
# right output.
x0 = list_func_t_to_list_t(["abc"])[0]
#? str()
#?
x0
x2 = list_func_t_to_list_t([tpl])[0]
#? tuple()
#?
x2
x3 = list_func_t_to_list_t([tpl_typed])[0]
#? tuple()
#?
x3
x4 = list_func_t_to_list_t([collection])[0]
#? dict()
#?
x4
x5 = list_func_t_to_list_t([collection_typed])[0]
#? dict()
#?
x5
x6 = list_func_t_to_list_t([custom_generic])[0]
#? CustomGeneric()
#?
x6
x7 = list_func_t_to_list_t([plain_instance])[0]
#? PlainClass()
#?
x7
for a in list_func_t_to_list_t([12]):
#? int()
#?
a

View File

@@ -60,6 +60,27 @@ for b in list_type_t_to_list_t(list_of_int_type):
b
# Test construction of nested generic tuple return parameters
def list_t_to_list_tuple_t(the_list: List[T]) -> List[Tuple[T]]:
return [(x,) for x in the_list]
x1t = list_t_to_list_tuple_t(list_of_ints)[0][0]
#? int()
x1t
for c1 in list_t_to_list_tuple_t(list_of_ints):
#? int()
c1[0]
for c2, in list_t_to_list_tuple_t(list_of_ints):
#? int()
c2
# Test handling of nested tuple input parameters
def list_tuple_t_to_tuple_list_t(the_list: List[Tuple[T]]) -> Tuple[List[T], ...]:
return tuple(list(x) for x in the_list)
@@ -82,11 +103,12 @@ for b in list_tuple_t_elipsis_to_tuple_list_t(list_of_int_tuple_elipsis):
b[0]
def foo(x: T) -> T:
# Test handling of nested callables
def foo(x: int) -> int:
return x
list_of_funcs = [foo] # type: List[Callable[[T], T]]
list_of_funcs = [foo] # type: List[Callable[[int], int]]
def list_func_t_to_list_func_type_t(the_list: List[Callable[[T], T]]) -> List[Callable[[Type[T]], T]]:
def adapt(func: Callable[[T], T]) -> Callable[[Type[T]], T]:
@@ -101,6 +123,21 @@ for b in list_func_t_to_list_func_type_t(list_of_funcs):
b(int)
def bar(*a, **k) -> int:
return len(a) + len(k)
list_of_funcs_2 = [bar] # type: List[Callable[..., int]]
def list_func_t_passthrough(the_list: List[Callable[..., T]]) -> List[Callable[..., T]]:
return the_list
for b in list_func_t_passthrough(list_of_funcs_2):
#? int()
b(None, x="x")
mapping_int_str = {42: 'a'} # type: Dict[int, str]
# Test that mappings (that have more than one parameter) are handled

View File

@@ -283,6 +283,18 @@ def testnewtype2(y):
y
#? []
y.
# The type of a NewType is equivalent to the type of its underlying type.
MyInt = typing.NewType('MyInt', int)
x = type(MyInt)
#? type.mro
x.mro
PlainInt = int
y = type(PlainInt)
#? type.mro
y.mro
# python > 2.7
class TestDefaultDict(typing.DefaultDict[str, int]):

View File

@@ -103,3 +103,15 @@ while True:
bar = bar # type: bar
#? int()
bar
class Comprehension:
def __init__(self, foo):
self.foo = foo
def update(self):
self.foo = (self.foo,)
#? int() tuple()
Comprehension(1).foo[0]

View File

@@ -1,11 +0,0 @@
#! ['class ObjectDoesNotExist']
from django.core.exceptions import ObjectDoesNotExist
import django
#? ['get_version']
django.get_version
from django.conf import settings
#? ['configured']
settings.configured

View File

@@ -115,3 +115,18 @@ def test_docstring_decorator(goto_or_help_or_infer, skip_python2):
doc = d.docstring()
assert doc == 'FunctionType(*args: Any, **kwargs: Any) -> Any\n\nhello'
@pytest.mark.parametrize('code', ['', '\n', ' '])
def test_empty(Script, code):
assert not Script(code).help(1, 0)
@pytest.mark.parametrize('code', ['f()', '(bar or baz)', 'f[3]'])
def test_no_help_for_operator(Script, code):
assert not Script(code).help()
@pytest.mark.parametrize('code', ['()', '(1,)', '[]', '[1]', 'f[]'])
def test_help_for_operator(Script, code):
assert Script(code).help()

View File

@@ -20,6 +20,12 @@ def test_django_default_project(Script):
assert script._inference_state.project._django is True
def test_django_default_project_of_file(Script):
project = get_default_project(__file__)
d = os.path.dirname
assert project._path == d(d(d(__file__)))
def test_interpreter_project_path():
# Run from anywhere it should be the cwd.
dir = os.path.join(root_dir, 'test')

View File

@@ -36,7 +36,7 @@ unspecified = %s
""" % (case, sorted(d - a), sorted(a - d))
def test_completion(case, monkeypatch, environment, has_typing):
def test_completion(case, monkeypatch, environment, has_typing, has_django):
skip_reason = case.get_skip_reason(environment)
if skip_reason is not None:
pytest.skip(skip_reason)
@@ -47,6 +47,8 @@ def test_completion(case, monkeypatch, environment, has_typing):
_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.')
if (not has_django or environment.version_info.major == 2) and case.path.endswith('django.py'):
pytest.skip('Needs django to be installed to run this test.')
repo_root = helpers.root_dir
monkeypatch.chdir(os.path.join(repo_root, 'jedi'))
case.run(assert_case_equal, environment)