mirror of
https://github.com/davidhalter/jedi.git
synced 2025-12-16 10:37:52 +08:00
Merge branch 'master' into fix-nested-tuple-argument
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
|||||||
[submodule "jedi/third_party/typeshed"]
|
[submodule "jedi/third_party/typeshed"]
|
||||||
path = jedi/third_party/typeshed
|
path = jedi/third_party/typeshed
|
||||||
url = https://github.com/davidhalter/typeshed.git
|
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
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
Changelog
|
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)
|
0.17.0 (2020-04-14)
|
||||||
+++++++++++++++++++
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ include requirements.txt
|
|||||||
include jedi/parser/python/grammar*.txt
|
include jedi/parser/python/grammar*.txt
|
||||||
recursive-include jedi/third_party *.pyi
|
recursive-include jedi/third_party *.pyi
|
||||||
include jedi/third_party/typeshed/LICENSE
|
include jedi/third_party/typeshed/LICENSE
|
||||||
|
include jedi/third_party/django-stubs/LICENSE.txt
|
||||||
include jedi/third_party/typeshed/README
|
include jedi/third_party/typeshed/README
|
||||||
recursive-include test *
|
recursive-include test *
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from test.helpers import test_dir
|
|||||||
|
|
||||||
collect_ignore = [
|
collect_ignore = [
|
||||||
'setup.py',
|
'setup.py',
|
||||||
'__main__.py',
|
'jedi/__main__.py',
|
||||||
'jedi/inference/compiled/subprocess/__main__.py',
|
'jedi/inference/compiled/subprocess/__main__.py',
|
||||||
'build/',
|
'build/',
|
||||||
'test/examples',
|
'test/examples',
|
||||||
@@ -147,6 +147,12 @@ def has_typing(environment):
|
|||||||
return bool(script.infer())
|
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')
|
@pytest.fixture(scope='session')
|
||||||
def jedi_path():
|
def jedi_path():
|
||||||
return os.path.dirname(__file__)
|
return os.path.dirname(__file__)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ ad
|
|||||||
load
|
load
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '0.17.0'
|
__version__ = '0.17.1'
|
||||||
|
|
||||||
from jedi.api import Script, Interpreter, set_debug_function, \
|
from jedi.api import Script, Interpreter, set_debug_function, \
|
||||||
preload_module, names
|
preload_module, names
|
||||||
|
|||||||
@@ -44,20 +44,29 @@ def _complete():
|
|||||||
import jedi
|
import jedi
|
||||||
import pdb
|
import pdb
|
||||||
|
|
||||||
|
if '-d' in sys.argv:
|
||||||
|
sys.argv.remove('-d')
|
||||||
|
jedi.set_debug_function()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for c in jedi.Script(sys.argv[2]).complete():
|
completions = jedi.Script(sys.argv[2]).complete()
|
||||||
|
for c in completions:
|
||||||
c.docstring()
|
c.docstring()
|
||||||
c.type
|
c.type
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(repr(e))
|
||||||
pdb.post_mortem()
|
pdb.post_mortem()
|
||||||
|
else:
|
||||||
|
print(completions)
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) == 2 and sys.argv[1] == 'repl':
|
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
|
# 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.
|
# something else. So just use the keyword ``repl`` for now.
|
||||||
print(join(dirname(abspath(__file__)), 'api', 'replstartup.py'))
|
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()
|
_start_linter()
|
||||||
elif len(sys.argv) > 1 and sys.argv[1] == '_complete':
|
elif len(sys.argv) > 1 and sys.argv[1] == '_complete':
|
||||||
_complete()
|
_complete()
|
||||||
|
else:
|
||||||
|
print('Command not implemented: %s' % sys.argv[1])
|
||||||
|
|||||||
@@ -339,6 +339,13 @@ try:
|
|||||||
except NameError:
|
except NameError:
|
||||||
PermissionError = IOError
|
PermissionError = IOError
|
||||||
|
|
||||||
|
try:
|
||||||
|
NotADirectoryError = NotADirectoryError
|
||||||
|
except NameError:
|
||||||
|
class NotADirectoryError(Exception):
|
||||||
|
# Don't implement this for Python 2 anymore.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def no_unicode_pprint(dct):
|
def no_unicode_pprint(dct):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -472,9 +472,20 @@ class Script(object):
|
|||||||
if definitions:
|
if definitions:
|
||||||
return definitions
|
return definitions
|
||||||
leaf = self._module_node.get_leaf_for_position((line, column))
|
leaf = self._module_node.get_leaf_for_position((line, column))
|
||||||
if leaf.type in ('keyword', 'operator', 'error_leaf'):
|
if leaf is not None and leaf.type in ('keyword', 'operator', 'error_leaf'):
|
||||||
reserved = self._inference_state.grammar._pgen_grammar.reserved_syntax_strings.keys()
|
def need_pydoc():
|
||||||
if leaf.value in reserved:
|
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)
|
name = KeywordName(self._inference_state, leaf.value)
|
||||||
return [classes.Name(self._inference_state, name)]
|
return [classes.Name(self._inference_state, name)]
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ class BaseName(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def module_path(self):
|
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()
|
module = self._get_module_context()
|
||||||
if module.is_stub() or not module.is_compiled():
|
if module.is_stub() or not module.is_compiled():
|
||||||
# Compiled modules should not return a module path even if they
|
# Compiled modules should not return a module path even if they
|
||||||
@@ -168,7 +172,7 @@ class BaseName(object):
|
|||||||
>>> defs[3]
|
>>> defs[3]
|
||||||
'function'
|
'function'
|
||||||
|
|
||||||
Valid values for are ``module``, ``class``, ``instance``, ``function``,
|
Valid values for type are ``module``, ``class``, ``instance``, ``function``,
|
||||||
``param``, ``path``, ``keyword`` and ``statement``.
|
``param``, ``path``, ``keyword`` and ``statement``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -245,8 +249,8 @@ class BaseName(object):
|
|||||||
Document for function f.
|
Document for function f.
|
||||||
|
|
||||||
Notice that useful extra information is added to the actual
|
Notice that useful extra information is added to the actual
|
||||||
docstring. For function, it is signature. If you need
|
docstring, e.g. function signatures are prepended to their docstrings.
|
||||||
actual docstring, use ``raw=True`` instead.
|
If you need the actual docstring, use ``raw=True`` instead.
|
||||||
|
|
||||||
>>> print(script.infer(1, len('def f'))[0].docstring(raw=True))
|
>>> print(script.infer(1, len('def f'))[0].docstring(raw=True))
|
||||||
Document for function f.
|
Document for function f.
|
||||||
@@ -665,7 +669,7 @@ class Completion(BaseName):
|
|||||||
|
|
||||||
def docstring(self, raw=False, fast=True):
|
def docstring(self, raw=False, fast=True):
|
||||||
"""
|
"""
|
||||||
Documentated under :meth:`BaseName.docstring`.
|
Documented under :meth:`BaseName.docstring`.
|
||||||
"""
|
"""
|
||||||
if self._like_name_length >= 3:
|
if self._like_name_length >= 3:
|
||||||
# In this case we can just resolve the like name, because we
|
# In this case we can just resolve the like name, because we
|
||||||
@@ -703,7 +707,7 @@ class Completion(BaseName):
|
|||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
"""
|
"""
|
||||||
Documentated under :meth:`BaseName.type`.
|
Documented under :meth:`BaseName.type`.
|
||||||
"""
|
"""
|
||||||
# Purely a speed optimization.
|
# Purely a speed optimization.
|
||||||
if self._cached_name is not None:
|
if self._cached_name is not None:
|
||||||
@@ -734,8 +738,7 @@ class Name(BaseName):
|
|||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2
|
stacklevel=2
|
||||||
)
|
)
|
||||||
position = '' if self.in_builtin_module else '@%s' % self.line
|
return "%s:%s" % (self.module_name, self.description)
|
||||||
return "%s:%s%s" % (self.module_name, self.description, position)
|
|
||||||
|
|
||||||
@memoize_method
|
@memoize_method
|
||||||
def defined_names(self):
|
def defined_names(self):
|
||||||
@@ -798,7 +801,7 @@ class BaseSignature(Name):
|
|||||||
Returns a text representation of the signature. This could for example
|
Returns a text representation of the signature. This could for example
|
||||||
look like ``foo(bar, baz: int, **kwargs)``.
|
look like ``foo(bar, baz: int, **kwargs)``.
|
||||||
|
|
||||||
:return str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self._signature.to_string()
|
return self._signature.to_string()
|
||||||
|
|
||||||
@@ -865,7 +868,7 @@ class ParamName(Name):
|
|||||||
Returns a simple representation of a param, like
|
Returns a simple representation of a param, like
|
||||||
``f: Callable[..., Any]``.
|
``f: Callable[..., Any]``.
|
||||||
|
|
||||||
:rtype: :class:`str`
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self._name.to_string()
|
return self._name.to_string()
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from jedi._compatibility import FileNotFoundError, PermissionError, \
|
from jedi._compatibility import FileNotFoundError, PermissionError, \
|
||||||
IsADirectoryError
|
IsADirectoryError, NotADirectoryError
|
||||||
from jedi import debug
|
from jedi import debug
|
||||||
from jedi.api.environment import get_cached_default_environment, create_environment
|
from jedi.api.environment import get_cached_default_environment, create_environment
|
||||||
from jedi.api.exceptions import WrongVersion
|
from jedi.api.exceptions import WrongVersion
|
||||||
@@ -383,6 +383,8 @@ def get_default_project(path=None):
|
|||||||
return Project.load(dir)
|
return Project.load(dir)
|
||||||
except (FileNotFoundError, IsADirectoryError, PermissionError):
|
except (FileNotFoundError, IsADirectoryError, PermissionError):
|
||||||
pass
|
pass
|
||||||
|
except NotADirectoryError:
|
||||||
|
continue
|
||||||
|
|
||||||
if first_no_init_file is None:
|
if first_no_init_file is None:
|
||||||
if os.path.exists(os.path.join(dir, '__init__.py')):
|
if os.path.exists(os.path.join(dir, '__init__.py')):
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class DefineGenericBase(LazyValueWrapper):
|
|||||||
for generic_set in self.get_generics():
|
for generic_set in self.get_generics():
|
||||||
values = NO_VALUES
|
values = NO_VALUES
|
||||||
for generic in generic_set:
|
for generic in generic_set:
|
||||||
if isinstance(generic, (GenericClass, TypeVar)):
|
if isinstance(generic, (DefineGenericBase, TypeVar)):
|
||||||
result = generic.define_generics(type_var_dict)
|
result = generic.define_generics(type_var_dict)
|
||||||
values |= result
|
values |= result
|
||||||
if result != ValueSet({generic}):
|
if result != ValueSet({generic}):
|
||||||
|
|||||||
@@ -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__))))
|
_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')
|
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(
|
_IMPORT_MAP = dict(
|
||||||
_collections='collections',
|
_collections='collections',
|
||||||
@@ -173,6 +175,13 @@ def _try_to_load_stub(inference_state, import_names, python_value_set,
|
|||||||
)
|
)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return m
|
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.
|
# 2. Try to load pyi files next to py files.
|
||||||
for c in python_value_set:
|
for c in python_value_set:
|
||||||
|
|||||||
@@ -226,9 +226,24 @@ class TypingClassValueWithIndex(_TypingClassMixin, TypingValueWithIndex):
|
|||||||
|
|
||||||
elif annotation_name == 'Callable':
|
elif annotation_name == 'Callable':
|
||||||
if len(annotation_generics) == 2:
|
if len(annotation_generics) == 2:
|
||||||
return annotation_generics[1].infer_type_vars(
|
if is_class_value:
|
||||||
value_set.execute_annotation(),
|
# 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(),
|
||||||
|
)
|
||||||
|
|
||||||
elif annotation_name == 'Tuple':
|
elif annotation_name == 'Tuple':
|
||||||
tuple_annotation = self.get_annotated_class_object()
|
tuple_annotation = self.get_annotated_class_object()
|
||||||
@@ -421,6 +436,10 @@ class NewType(Value):
|
|||||||
self._type_value_set = type_value_set
|
self._type_value_set = type_value_set
|
||||||
self.tree_node = tree_node
|
self.tree_node = tree_node
|
||||||
|
|
||||||
|
def py__class__(self):
|
||||||
|
c, = self._type_value_set.py__class__()
|
||||||
|
return c
|
||||||
|
|
||||||
def py__call__(self, arguments):
|
def py__call__(self, arguments):
|
||||||
return self._type_value_set.execute_annotation()
|
return self._type_value_set.execute_annotation()
|
||||||
|
|
||||||
|
|||||||
@@ -192,13 +192,17 @@ class Sequence(LazyAttributeOverwrite, IterableMixin):
|
|||||||
def _get_generics(self):
|
def _get_generics(self):
|
||||||
return (self.merge_types_of_iterate().py__class__(),)
|
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):
|
def _get_wrapped_value(self):
|
||||||
from jedi.inference.gradual.base import GenericClass
|
from jedi.inference.gradual.base import GenericClass
|
||||||
from jedi.inference.gradual.generics import TupleGenericManager
|
from jedi.inference.gradual.generics import TupleGenericManager
|
||||||
klass = compiled.builtin_from_name(self.inference_state, self.array_type)
|
klass = compiled.builtin_from_name(self.inference_state, self.array_type)
|
||||||
c, = GenericClass(
|
c, = GenericClass(
|
||||||
klass,
|
klass,
|
||||||
TupleGenericManager(self._get_generics())
|
TupleGenericManager(self._cached_generics())
|
||||||
).execute_annotation()
|
).execute_annotation()
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|||||||
@@ -186,11 +186,12 @@ class ClassMixin(object):
|
|||||||
mro.append(cls_new)
|
mro.append(cls_new)
|
||||||
yield 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):
|
||||||
metaclasses = self.get_metaclasses()
|
if include_metaclasses:
|
||||||
if metaclasses:
|
metaclasses = self.get_metaclasses()
|
||||||
for f in self.get_metaclass_filters(metaclasses):
|
if metaclasses:
|
||||||
yield f
|
for f in self.get_metaclass_filters(metaclasses):
|
||||||
|
yield f
|
||||||
|
|
||||||
for cls in self.py__mro__():
|
for cls in self.py__mro__():
|
||||||
if cls.is_compiled():
|
if cls.is_compiled():
|
||||||
|
|||||||
130
jedi/plugins/django.py
Normal file
130
jedi/plugins/django.py
Normal 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
|
||||||
@@ -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 stdlib
|
||||||
from jedi.plugins import flask
|
from jedi.plugins import flask
|
||||||
from jedi.plugins import pytest
|
from jedi.plugins import pytest
|
||||||
|
from jedi.plugins import django
|
||||||
from jedi.plugins import plugin_manager
|
from jedi.plugins import plugin_manager
|
||||||
|
|
||||||
|
|
||||||
plugin_manager.register(stdlib, flask, pytest)
|
plugin_manager.register(stdlib, flask, pytest, django)
|
||||||
|
|||||||
1
jedi/third_party/django-stubs
vendored
Submodule
1
jedi/third_party/django-stubs
vendored
Submodule
Submodule jedi/third_party/django-stubs added at 92c8dfc93f
3
setup.py
3
setup.py
@@ -19,6 +19,8 @@ with open('requirements.txt') as f:
|
|||||||
|
|
||||||
assert os.path.isfile("jedi/third_party/typeshed/LICENSE"), \
|
assert os.path.isfile("jedi/third_party/typeshed/LICENSE"), \
|
||||||
"Please download the typeshed submodule first (Hint: git submodule update --init)"
|
"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',
|
setup(name='jedi',
|
||||||
version=version,
|
version=version,
|
||||||
@@ -43,6 +45,7 @@ setup(name='jedi',
|
|||||||
'docopt',
|
'docopt',
|
||||||
# coloroma for colored debug output
|
# coloroma for colored debug output
|
||||||
'colorama',
|
'colorama',
|
||||||
|
'Django<3.1', # For now pin this.
|
||||||
],
|
],
|
||||||
'qa': [
|
'qa': [
|
||||||
'flake8==3.7.9',
|
'flake8==3.7.9',
|
||||||
|
|||||||
2
sith.py
2
sith.py
@@ -123,7 +123,7 @@ class TestCase(object):
|
|||||||
with open(self.path) as f:
|
with open(self.path) as f:
|
||||||
self.script = jedi.Script(f.read(), path=self.path)
|
self.script = jedi.Script(f.read(), path=self.path)
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if self.operation == 'goto_assignments':
|
if self.operation == 'goto':
|
||||||
kwargs['follow_imports'] = random.choice([False, True])
|
kwargs['follow_imports'] = random.choice([False, True])
|
||||||
|
|
||||||
self.objects = getattr(self.script, self.operation)(self.line, self.column, **kwargs)
|
self.objects = getattr(self.script, self.operation)(self.line, self.column, **kwargs)
|
||||||
|
|||||||
168
test/completion/django.py
Normal file
168
test/completion/django.py
Normal 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']
|
||||||
@@ -207,40 +207,36 @@ for a in list_func_t_to_list_t(12):
|
|||||||
a
|
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]
|
x0 = list_func_t_to_list_t(["abc"])[0]
|
||||||
#? str()
|
#?
|
||||||
x0
|
x0
|
||||||
|
|
||||||
x2 = list_func_t_to_list_t([tpl])[0]
|
x2 = list_func_t_to_list_t([tpl])[0]
|
||||||
#? tuple()
|
#?
|
||||||
x2
|
x2
|
||||||
|
|
||||||
x3 = list_func_t_to_list_t([tpl_typed])[0]
|
x3 = list_func_t_to_list_t([tpl_typed])[0]
|
||||||
#? tuple()
|
#?
|
||||||
x3
|
x3
|
||||||
|
|
||||||
x4 = list_func_t_to_list_t([collection])[0]
|
x4 = list_func_t_to_list_t([collection])[0]
|
||||||
#? dict()
|
#?
|
||||||
x4
|
x4
|
||||||
|
|
||||||
x5 = list_func_t_to_list_t([collection_typed])[0]
|
x5 = list_func_t_to_list_t([collection_typed])[0]
|
||||||
#? dict()
|
#?
|
||||||
x5
|
x5
|
||||||
|
|
||||||
x6 = list_func_t_to_list_t([custom_generic])[0]
|
x6 = list_func_t_to_list_t([custom_generic])[0]
|
||||||
#? CustomGeneric()
|
#?
|
||||||
x6
|
x6
|
||||||
|
|
||||||
x7 = list_func_t_to_list_t([plain_instance])[0]
|
x7 = list_func_t_to_list_t([plain_instance])[0]
|
||||||
#? PlainClass()
|
#?
|
||||||
x7
|
x7
|
||||||
|
|
||||||
for a in list_func_t_to_list_t([12]):
|
for a in list_func_t_to_list_t([12]):
|
||||||
#? int()
|
#?
|
||||||
a
|
a
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,27 @@ for b in list_type_t_to_list_t(list_of_int_type):
|
|||||||
b
|
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], ...]:
|
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)
|
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]
|
b[0]
|
||||||
|
|
||||||
|
|
||||||
def foo(x: T) -> T:
|
# Test handling of nested callables
|
||||||
|
def foo(x: int) -> int:
|
||||||
return x
|
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 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]:
|
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)
|
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]
|
mapping_int_str = {42: 'a'} # type: Dict[int, str]
|
||||||
|
|
||||||
# Test that mappings (that have more than one parameter) are handled
|
# Test that mappings (that have more than one parameter) are handled
|
||||||
|
|||||||
@@ -283,6 +283,18 @@ def testnewtype2(y):
|
|||||||
y
|
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
|
# python > 2.7
|
||||||
|
|
||||||
class TestDefaultDict(typing.DefaultDict[str, int]):
|
class TestDefaultDict(typing.DefaultDict[str, int]):
|
||||||
|
|||||||
@@ -103,3 +103,15 @@ while True:
|
|||||||
bar = bar # type: bar
|
bar = bar # type: bar
|
||||||
#? int()
|
#? int()
|
||||||
bar
|
bar
|
||||||
|
|
||||||
|
|
||||||
|
class Comprehension:
|
||||||
|
def __init__(self, foo):
|
||||||
|
self.foo = foo
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.foo = (self.foo,)
|
||||||
|
|
||||||
|
|
||||||
|
#? int() tuple()
|
||||||
|
Comprehension(1).foo[0]
|
||||||
|
|||||||
11
test/completion/thirdparty/django_.py
vendored
11
test/completion/thirdparty/django_.py
vendored
@@ -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
|
|
||||||
@@ -115,3 +115,18 @@ def test_docstring_decorator(goto_or_help_or_infer, skip_python2):
|
|||||||
|
|
||||||
doc = d.docstring()
|
doc = d.docstring()
|
||||||
assert doc == 'FunctionType(*args: Any, **kwargs: Any) -> Any\n\nhello'
|
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()
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ def test_django_default_project(Script):
|
|||||||
assert script._inference_state.project._django is True
|
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():
|
def test_interpreter_project_path():
|
||||||
# Run from anywhere it should be the cwd.
|
# Run from anywhere it should be the cwd.
|
||||||
dir = os.path.join(root_dir, 'test')
|
dir = os.path.join(root_dir, 'test')
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ unspecified = %s
|
|||||||
""" % (case, sorted(d - a), sorted(a - d))
|
""" % (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)
|
skip_reason = case.get_skip_reason(environment)
|
||||||
if skip_reason is not None:
|
if skip_reason is not None:
|
||||||
pytest.skip(skip_reason)
|
pytest.skip(skip_reason)
|
||||||
@@ -47,6 +47,8 @@ def test_completion(case, monkeypatch, environment, has_typing):
|
|||||||
_CONTAINS_TYPING = ('pep0484_typing', 'pep0484_comments', 'pep0526_variables')
|
_CONTAINS_TYPING = ('pep0484_typing', 'pep0484_comments', 'pep0526_variables')
|
||||||
if not has_typing and any(x in case.path for x in _CONTAINS_TYPING):
|
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.')
|
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
|
repo_root = helpers.root_dir
|
||||||
monkeypatch.chdir(os.path.join(repo_root, 'jedi'))
|
monkeypatch.chdir(os.path.join(repo_root, 'jedi'))
|
||||||
case.run(assert_case_equal, environment)
|
case.run(assert_case_equal, environment)
|
||||||
|
|||||||
Reference in New Issue
Block a user