mirror of
https://github.com/davidhalter/jedi.git
synced 2026-11-05 13:42:51 +08:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142bd1d0be | ||
|
|
575c6ab378 | ||
|
|
c71dead915 | ||
|
|
091735802d | ||
|
|
102700116c | ||
|
|
79d9764bce | ||
|
|
3f7e8adddc | ||
|
|
d8efff8e30 | ||
|
|
2e3e905707 | ||
|
|
558135e8f3 | ||
|
|
f37a9f9feb | ||
|
|
ed47f89162 | ||
|
|
112d255414 | ||
|
|
5c37b736a3 | ||
|
|
19236b472b | ||
|
|
ef5d361121 |
No files matched your search
@@ -66,6 +66,11 @@ Code Contributors
|
||||
- Martin Vielsmaier (@moser) <martin@vielsmaier.net>
|
||||
- TingJia Wu (@WutingjiaX) <wutingjia@bytedance.com>
|
||||
- Nguyễn Hồng Quân <ng.hong.quan@gmail.com>
|
||||
- haoran3160-afk (@haoran3160-afk)
|
||||
- Eric3-jp (@Eric3-jp) (with OpenAI Codex assistance)
|
||||
- Dhanraj E (@Dhanu-dynamic) <dhanraj@labs.digiscrypt.com>
|
||||
- Charan Rathore (@charan-rathore)
|
||||
- Abdullah Ahmed (@abdull-ah-med)
|
||||
|
||||
And a few more "anonymous" contributors.
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ Changelog
|
||||
Unreleased
|
||||
++++++++++
|
||||
|
||||
0.20.1 (2026-10-12)
|
||||
+++++++++++++++++++
|
||||
|
||||
- Avoid loading an executable path from `project.json`
|
||||
- Avoid changing the global recursion limit
|
||||
- Support for async pytest fixtures
|
||||
|
||||
0.20.0 (2026-05-02)
|
||||
+++++++++++++++++++
|
||||
|
||||
|
||||
+17
-6
@@ -7,7 +7,6 @@ Alternatively, if you don't need a custom function and are happy with printing
|
||||
debug messages to stdout, simply call :func:`set_debug_function` without
|
||||
arguments.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import parso
|
||||
@@ -21,7 +20,7 @@ from jedi.file_io import KnownContentFileIO
|
||||
from jedi.api import classes
|
||||
from jedi.api import interpreter
|
||||
from jedi.api import helpers
|
||||
from jedi.api.helpers import validate_line_column
|
||||
from jedi.api.helpers import validate_line_column, recursion_limit
|
||||
from jedi.api.completion import Completion, search_in_module
|
||||
from jedi.api.keywords import KeywordName
|
||||
from jedi.api.environment import InterpreterEnvironment
|
||||
@@ -43,10 +42,6 @@ from jedi.inference.gradual.conversion import convert_names, convert_values
|
||||
from jedi.inference.gradual.utils import load_proper_stub_module
|
||||
from jedi.inference.utils import to_list
|
||||
|
||||
# Jedi uses lots and lots of recursion. By setting this a little bit higher, we
|
||||
# can remove some "maximum recursion depth" errors.
|
||||
sys.setrecursionlimit(3000)
|
||||
|
||||
|
||||
class Script:
|
||||
"""
|
||||
@@ -97,6 +92,7 @@ class Script:
|
||||
references works well, because the right folder is searched. There are
|
||||
also ways to modify the sys path and other things.
|
||||
"""
|
||||
@recursion_limit
|
||||
def __init__(self, code=None, *, path=None, environment=None, project=None):
|
||||
self._orig_path = path
|
||||
if isinstance(path, str):
|
||||
@@ -191,6 +187,7 @@ class Script:
|
||||
self._inference_state.environment,
|
||||
)
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def complete(self, line=None, column=None, *, fuzzy=False):
|
||||
"""
|
||||
@@ -214,6 +211,7 @@ class Script:
|
||||
)
|
||||
return completion.complete()
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def infer(self, line=None, column=None, *, only_stubs=False, prefer_stubs=False):
|
||||
"""
|
||||
@@ -259,6 +257,7 @@ class Script:
|
||||
# the API.
|
||||
return helpers.sorted_definitions(set(defs))
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def goto(self, line=None, column=None, *, follow_imports=False, follow_builtin_imports=False,
|
||||
only_stubs=False, prefer_stubs=False):
|
||||
@@ -324,6 +323,7 @@ class Script:
|
||||
"""
|
||||
return self._search_func(string, all_scopes=all_scopes)
|
||||
|
||||
@recursion_limit
|
||||
@to_list
|
||||
def _search_func(self, string, all_scopes=False, complete=False, fuzzy=False):
|
||||
names = self._names(all_scopes=all_scopes)
|
||||
@@ -353,6 +353,7 @@ class Script:
|
||||
"""
|
||||
return self._search_func(string, complete=True, **kwargs)
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def help(self, line=None, column=None):
|
||||
"""
|
||||
@@ -397,6 +398,7 @@ class Script:
|
||||
return [classes.Name(self._inference_state, name)]
|
||||
return []
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def get_references(self, line=None, column=None, **kwargs):
|
||||
"""
|
||||
@@ -428,6 +430,7 @@ class Script:
|
||||
return helpers.sorted_definitions(definitions)
|
||||
return _references(**kwargs)
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def get_signatures(self, line=None, column=None):
|
||||
"""
|
||||
@@ -466,6 +469,7 @@ class Script:
|
||||
return [classes.Signature(self._inference_state, signature, call_details)
|
||||
for signature in definitions.get_signatures()]
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def get_context(self, line=None, column=None):
|
||||
"""
|
||||
@@ -502,6 +506,8 @@ class Script:
|
||||
tree_name = name.tree_name
|
||||
if tree_name is not None: # Happens with lambdas.
|
||||
scope = tree_name.get_definition()
|
||||
if scope.parent.type in ('async_stmt', 'async_funcdef'):
|
||||
scope = scope.parent
|
||||
if scope.start_pos[1] < column:
|
||||
break
|
||||
definition = definition.parent()
|
||||
@@ -541,6 +547,7 @@ class Script:
|
||||
finally:
|
||||
self._inference_state.is_analysis = False
|
||||
|
||||
@recursion_limit
|
||||
def get_names(self, **kwargs):
|
||||
"""
|
||||
Returns names defined in the current file.
|
||||
@@ -579,6 +586,7 @@ class Script:
|
||||
]
|
||||
return sorted(defs, key=lambda x: x.start_pos)
|
||||
|
||||
@recursion_limit
|
||||
def rename(self, line=None, column=None, *, new_name):
|
||||
"""
|
||||
Renames all references of the variable under the cursor.
|
||||
@@ -591,6 +599,7 @@ class Script:
|
||||
definitions = self.get_references(line, column, include_builtins=False)
|
||||
return refactoring.rename(self._inference_state, definitions, new_name)
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def extract_variable(self, line, column, *, new_name, until_line=None, until_column=None):
|
||||
"""
|
||||
@@ -630,6 +639,7 @@ class Script:
|
||||
new_name, (line, column), until_pos
|
||||
)
|
||||
|
||||
@recursion_limit
|
||||
@validate_line_column
|
||||
def extract_function(self, line, column, *, new_name, until_line=None, until_column=None):
|
||||
"""
|
||||
@@ -677,6 +687,7 @@ class Script:
|
||||
new_name, (line, column), until_pos
|
||||
)
|
||||
|
||||
@recursion_limit
|
||||
def inline(self, line=None, column=None):
|
||||
"""
|
||||
Inlines a variable under the cursor. This is basically the opposite of
|
||||
|
||||
+16
-3
@@ -28,7 +28,7 @@ from jedi.inference.gradual.conversion import convert_names, convert_values
|
||||
from jedi.inference.base_value import ValueSet, HasNoContext
|
||||
from jedi.api.keywords import KeywordName
|
||||
from jedi.api import completion_cache
|
||||
from jedi.api.helpers import filter_follow_imports
|
||||
from jedi.api.helpers import filter_follow_imports, recursion_limit
|
||||
|
||||
|
||||
def _sort_names_by_start_pos(names):
|
||||
@@ -262,6 +262,7 @@ class BaseName:
|
||||
return last_leaf.end_pos
|
||||
return definition.end_pos
|
||||
|
||||
@recursion_limit
|
||||
def docstring(self, raw=False, fast=True):
|
||||
r"""
|
||||
Return a document string for this completion object.
|
||||
@@ -422,6 +423,7 @@ class BaseName:
|
||||
return tree_name.is_definition() and tree_name.parent.type == 'trailer'
|
||||
|
||||
@debug.increase_indent_cm('goto on name')
|
||||
@recursion_limit
|
||||
def goto(self, *, follow_imports=False, follow_builtin_imports=False,
|
||||
only_stubs=False, prefer_stubs=False):
|
||||
|
||||
@@ -452,6 +454,7 @@ class BaseName:
|
||||
for n in names]
|
||||
|
||||
@debug.increase_indent_cm('infer on name')
|
||||
@recursion_limit
|
||||
def infer(self, *, only_stubs=False, prefer_stubs=False):
|
||||
"""
|
||||
Like :meth:`.Script.infer`, it can be useful to understand which type
|
||||
@@ -551,7 +554,7 @@ class BaseName:
|
||||
return ''.join(lines[start_index:index + after + 1])
|
||||
|
||||
def _get_signatures(self, for_docstring=False):
|
||||
if self._name.api_type == 'property':
|
||||
if self._name.api_type in ('property', 'param'):
|
||||
return []
|
||||
if for_docstring and self._name.api_type == 'statement' and not self.is_stub():
|
||||
# For docstrings we don't resolve signatures if they are simple
|
||||
@@ -567,6 +570,7 @@ class BaseName:
|
||||
names = convert_names([self._name], prefer_stubs=True)
|
||||
return [sig for name in names for sig in name.infer().get_signatures()]
|
||||
|
||||
@recursion_limit
|
||||
def get_signatures(self):
|
||||
"""
|
||||
Returns all potential signatures for a function or a class. Multiple
|
||||
@@ -579,6 +583,7 @@ class BaseName:
|
||||
for s in self._get_signatures()
|
||||
]
|
||||
|
||||
@recursion_limit
|
||||
def execute(self):
|
||||
"""
|
||||
Uses type inference to "execute" this identifier and returns the
|
||||
@@ -588,6 +593,7 @@ class BaseName:
|
||||
"""
|
||||
return _values_to_definitions(self._name.infer().execute_with_values())
|
||||
|
||||
@recursion_limit
|
||||
def get_type_hint(self):
|
||||
"""
|
||||
Returns type hints like ``Iterable[int]`` or ``Union[int, str]``.
|
||||
@@ -670,6 +676,7 @@ class Completion(BaseName):
|
||||
"""
|
||||
return self._complete(False)
|
||||
|
||||
@recursion_limit
|
||||
def docstring(self, raw=False, fast=True):
|
||||
"""
|
||||
Documented under :meth:`BaseName.docstring`.
|
||||
@@ -752,6 +759,7 @@ class Name(BaseName):
|
||||
super().__init__(inference_state, definition)
|
||||
|
||||
@memoize_method
|
||||
@recursion_limit
|
||||
def defined_names(self):
|
||||
"""
|
||||
List sub-definitions (e.g., methods in class).
|
||||
@@ -797,6 +805,7 @@ class BaseSignature(Name):
|
||||
self._signature = signature
|
||||
|
||||
@property
|
||||
@recursion_limit
|
||||
def params(self):
|
||||
"""
|
||||
Returns definitions for all parameters that a signature defines.
|
||||
@@ -807,6 +816,7 @@ class BaseSignature(Name):
|
||||
return [ParamName(self._inference_state, n)
|
||||
for n in self._signature.get_param_names(resolve_stars=True)]
|
||||
|
||||
@recursion_limit
|
||||
def to_string(self):
|
||||
"""
|
||||
Returns a text representation of the signature. This could for example
|
||||
@@ -831,7 +841,7 @@ class Signature(BaseSignature):
|
||||
def index(self):
|
||||
"""
|
||||
Returns the param index of the current cursor position.
|
||||
Returns None if the index cannot be found in the curent call.
|
||||
Returns None if the index cannot be found in the current call.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
@@ -858,6 +868,7 @@ class Signature(BaseSignature):
|
||||
|
||||
|
||||
class ParamName(Name):
|
||||
@recursion_limit
|
||||
def infer_default(self):
|
||||
"""
|
||||
Returns default values like the ``1`` of ``def foo(x=1):``.
|
||||
@@ -866,6 +877,7 @@ class ParamName(Name):
|
||||
"""
|
||||
return _values_to_definitions(self._name.infer_default())
|
||||
|
||||
@recursion_limit
|
||||
def infer_annotation(self, **kwargs):
|
||||
"""
|
||||
:param execute_annotation: Default True; If False, values are not
|
||||
@@ -874,6 +886,7 @@ class ParamName(Name):
|
||||
"""
|
||||
return _values_to_definitions(self._name.infer_annotation(ignore_stars=True, **kwargs))
|
||||
|
||||
@recursion_limit
|
||||
def to_string(self):
|
||||
"""
|
||||
Returns a simple representation of a param, like
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
Helpers for the API
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from textwrap import dedent
|
||||
from itertools import chain
|
||||
from functools import wraps
|
||||
@@ -21,6 +23,48 @@ from jedi.parser_utils import get_parent_scope
|
||||
|
||||
CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name'])
|
||||
|
||||
RECURSION_LIMIT = 3000
|
||||
|
||||
|
||||
@contextmanager
|
||||
def higher_recursion_limit():
|
||||
"""Jedi needs a higher recursion limit for parsing and inference. Raise it
|
||||
only temporarily, so importing Jedi does not change global interpreter
|
||||
state and a user-defined higher limit is never lowered."""
|
||||
old_limit = sys.getrecursionlimit()
|
||||
if old_limit >= RECURSION_LIMIT:
|
||||
yield
|
||||
else:
|
||||
sys.setrecursionlimit(RECURSION_LIMIT)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
|
||||
|
||||
def recursion_limit(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
with higher_recursion_limit():
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def recursion_limit_generator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
with higher_recursion_limit():
|
||||
iterator = iter(func(*args, **kwargs))
|
||||
while True:
|
||||
with higher_recursion_limit():
|
||||
try:
|
||||
value = next(iterator)
|
||||
except StopIteration:
|
||||
return
|
||||
# Restore the caller's limit before yielding a result.
|
||||
yield value
|
||||
return wrapper
|
||||
|
||||
|
||||
def _start_match(string, like_name):
|
||||
return string.startswith(like_name)
|
||||
|
||||
+19
-2
@@ -15,7 +15,7 @@ from jedi import debug
|
||||
from jedi.api.environment import get_cached_default_environment, create_environment
|
||||
from jedi.api.exceptions import WrongVersion
|
||||
from jedi.api.completion import search_in_module
|
||||
from jedi.api.helpers import split_search_string, get_module_names
|
||||
from jedi.api.helpers import split_search_string, get_module_names, recursion_limit_generator
|
||||
from jedi.inference.imports import load_module_from_path, \
|
||||
load_namespace_from_path, iter_module_names
|
||||
from jedi.inference.sys_path import discover_buildout_paths
|
||||
@@ -86,6 +86,14 @@ class Project:
|
||||
version, data = json.load(f)
|
||||
|
||||
if version == 1:
|
||||
# A code base must not be able to enable the loading of its own
|
||||
# extensions or point Jedi at a binary to execute, see the docs
|
||||
# about security. The project file can be part of a checked out
|
||||
# repository, so these settings are ignored for loaded projects.
|
||||
if data.pop('load_unsafe_extensions', False):
|
||||
debug.warning('load_unsafe_extensions is ignored for loaded projects')
|
||||
if data.pop('environment_path', None) is not None:
|
||||
debug.warning('environment_path is ignored for loaded projects')
|
||||
return cls(**data)
|
||||
else:
|
||||
raise WrongVersion(
|
||||
@@ -99,6 +107,11 @@ class Project:
|
||||
data = dict(self.__dict__)
|
||||
data.pop('_environment', None)
|
||||
data.pop('_django', None) # TODO make django setting public?
|
||||
|
||||
# These are dropped anyway
|
||||
data.pop('_load_unsafe_extensions', None)
|
||||
data.pop('_environment_path', None)
|
||||
|
||||
data = {k.lstrip('_'): v for k, v in data.items()}
|
||||
data['path'] = str(data['path'])
|
||||
|
||||
@@ -242,7 +255,10 @@ class Project:
|
||||
def get_environment(self):
|
||||
if self._environment is None:
|
||||
if self._environment_path is not None:
|
||||
self._environment = create_environment(self._environment_path, safe=False)
|
||||
# environment_path can only be set by the user directly, it is
|
||||
# never loaded from a project file, so it is trusted.
|
||||
self._environment = create_environment(
|
||||
self._environment_path, safe=False)
|
||||
else:
|
||||
self._environment = get_cached_default_environment()
|
||||
return self._environment
|
||||
@@ -282,6 +298,7 @@ class Project:
|
||||
"""
|
||||
return self._search_func(string, complete=True, **kwargs)
|
||||
|
||||
@recursion_limit_generator
|
||||
@_try_to_skip_duplicates
|
||||
def _search_func(self, string, complete=False, all_scopes=False):
|
||||
# Using a Script is they easiest way to get an empty module context.
|
||||
|
||||
@@ -125,7 +125,7 @@ class InferenceState:
|
||||
debug.dbg('execute result: %s in %s', value_set, value)
|
||||
return value_set
|
||||
|
||||
# mypy doesn't suppport decorated propeties (https://github.com/python/mypy/issues/1362)
|
||||
# mypy doesn't support decorated properties (https://github.com/python/mypy/issues/1362)
|
||||
@property
|
||||
@inference_state_function_cache()
|
||||
def builtins_module(self):
|
||||
|
||||
@@ -110,7 +110,7 @@ def _check_for_setattr(instance):
|
||||
|
||||
def add_attribute_error(name_context, lookup_value, name):
|
||||
message = ('AttributeError: %s has no attribute %s.' % (lookup_value, name))
|
||||
# Check for __getattr__/__getattribute__ existance and issue a warning
|
||||
# Check for __getattr__/__getattribute__ existence and issue a warning
|
||||
# instead of an error, if that happens.
|
||||
typ = Error
|
||||
if lookup_value.is_instance() and not lookup_value.is_compiled():
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
import builtins
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, Any
|
||||
|
||||
from jedi.inference.compiled.getattr_static import getattr_static
|
||||
|
||||
@@ -46,7 +46,7 @@ ClassMethodDescriptorType = type(object_class_dict['__subclasshook__'])
|
||||
_sentinel = object()
|
||||
|
||||
# Maps Python syntax to the operator module.
|
||||
COMPARISON_OPERATORS = {
|
||||
COMPARISON_OPERATORS: dict[str, Any] = {
|
||||
'==': op.eq,
|
||||
'!=': op.ne,
|
||||
'is': op.is_,
|
||||
|
||||
@@ -416,10 +416,18 @@ def import_module(inference_state, import_names, parent_module_value, sys_path):
|
||||
if is_pkg is None:
|
||||
return NO_VALUES
|
||||
else:
|
||||
paths = parent_module_value.py__path__()
|
||||
if paths is None:
|
||||
# The module might not be a package.
|
||||
try:
|
||||
py__path__ = parent_module_value.py__path__
|
||||
except AttributeError:
|
||||
# Due to meta_path, the value can be pretty much any compiled value
|
||||
# and we're not guaranteed that the attribute is available, see
|
||||
# also https://github.com/davidhalter/jedi/issues/2118
|
||||
return NO_VALUES
|
||||
else:
|
||||
paths = py__path__()
|
||||
if paths is None:
|
||||
# The module might not be a package.
|
||||
return NO_VALUES
|
||||
|
||||
file_io_or_ns, is_pkg = inference_state.compiled_subprocess.get_module_info(
|
||||
string=import_names[-1],
|
||||
|
||||
@@ -170,6 +170,8 @@ class AbstractTreeName(AbstractNameDefinition):
|
||||
trailer = par.parent
|
||||
if trailer.type == 'arglist':
|
||||
trailer = trailer.parent
|
||||
if trailer.type == 'error_node':
|
||||
return []
|
||||
if trailer.type != 'classdef':
|
||||
if trailer.type == 'decorator':
|
||||
value_set = context.infer_node(trailer.children[1])
|
||||
|
||||
@@ -224,7 +224,7 @@ class _BaseTreeInstance(AbstractInstanceValue):
|
||||
elif isinstance(f, CompiledValueFilter):
|
||||
yield CompiledInstanceClassFilter(self, f)
|
||||
else:
|
||||
# Propably from the metaclass.
|
||||
# Probably from the metaclass.
|
||||
yield f
|
||||
|
||||
@inference_state_method_cache()
|
||||
|
||||
@@ -2,7 +2,9 @@ import sys
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from jedi.inference.arguments import ValuesArguments
|
||||
from jedi.inference.cache import inference_state_method_cache
|
||||
from jedi.inference.gradual.annotation import infer_return_types
|
||||
from jedi.inference.imports import goto_import, load_module_from_path
|
||||
from jedi.inference.filters import ParserTreeFilter
|
||||
from jedi.inference.base_value import NO_VALUES, ValueSet
|
||||
@@ -32,6 +34,19 @@ def execute(callback):
|
||||
def infer_anonymous_param(func):
|
||||
def get_returns(value):
|
||||
if value.tree_node.annotation is not None:
|
||||
if value.tree_node.parent.type == 'async_funcdef':
|
||||
# Async fixtures provide the awaited value, rather than a
|
||||
# coroutine. The annotation of an async generator describes
|
||||
# the iterator, so the value is what that iterator yields.
|
||||
result = infer_return_types(value, ValuesArguments([]))
|
||||
if value.as_context().is_generator():
|
||||
return ValueSet.from_sets(
|
||||
lazy_value.infer()
|
||||
for v in result
|
||||
for lazy_value in v.iterate(is_async=True)
|
||||
)
|
||||
return result
|
||||
|
||||
result = value.execute_with_values()
|
||||
if any(v.name.get_qualified_names(include_module_names=True)
|
||||
== ('typing', 'Generator')
|
||||
@@ -223,6 +238,8 @@ class FixtureFilter(ParserTreeFilter):
|
||||
if funcdef.type != "funcdef":
|
||||
return False
|
||||
decorated = funcdef.parent
|
||||
if decorated.type == "async_funcdef":
|
||||
decorated = decorated.parent
|
||||
if decorated.type != "decorated":
|
||||
return False
|
||||
decorators = decorated.children[0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Generator
|
||||
from typing import AsyncGenerator, Generator
|
||||
|
||||
import pytest
|
||||
from pytest import fixture
|
||||
@@ -208,3 +208,58 @@ def bla(admin_u
|
||||
def bla(admin_user):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
# -----------------
|
||||
# asynchronous fixtures
|
||||
# -----------------
|
||||
|
||||
@pytest.fixture
|
||||
async def async_fixture():
|
||||
return 1
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
async def async_annotated_fixture() -> str:
|
||||
return ''
|
||||
|
||||
|
||||
@fixture
|
||||
async def async_yield_fixture():
|
||||
yield 1.0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_annotated_opaque() -> int:
|
||||
return opaque_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_generator_annotated() -> AsyncGenerator[str, None]:
|
||||
yield opaque_value
|
||||
|
||||
|
||||
#? ['async_fixture']
|
||||
def test_async_fixture_completion(async_fixt
|
||||
pass
|
||||
|
||||
|
||||
def test_async_fixture_inference(async_fixture, async_annotated_fixture, async_yield_fixture):
|
||||
#? int()
|
||||
async_fixture
|
||||
#? str()
|
||||
async_annotated_fixture
|
||||
#? float()
|
||||
async_yield_fixture
|
||||
|
||||
|
||||
def test_async_fixture_annotation(async_annotated_opaque, async_generator_annotated):
|
||||
#? int()
|
||||
async_annotated_opaque
|
||||
#? str()
|
||||
async_generator_annotated
|
||||
|
||||
|
||||
#! 30 ['def async_fixture']
|
||||
def test_async_fixture_goto(async_fixture):
|
||||
pass
|
||||
@@ -3,6 +3,8 @@ Test all things related to the ``jedi.api`` module.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
@@ -231,6 +233,19 @@ def test_goto_follow_imports(Script):
|
||||
assert d.name == 'a'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('prefix', ['', 'def foo(bar): pass\n', 'def f():\n '])
|
||||
@pytest.mark.parametrize('call', ['foo(bar=1', 'foo(bar=1,', 'foo(foo(bar=1'])
|
||||
def test_goto_incomplete_named_argument(Script, prefix, call):
|
||||
code = prefix + call
|
||||
column = code.splitlines()[-1].index('bar')
|
||||
assert Script(code).goto(column=column) == []
|
||||
|
||||
|
||||
def test_goto_named_argument_in_complete_inner_call(Script):
|
||||
param, = Script('def foo(bar): pass\nfoo(foo(bar=1)').goto(2, 8)
|
||||
assert (param.name, param.line, param.column) == ('bar', 1, 8)
|
||||
|
||||
|
||||
def test_goto_module(Script):
|
||||
def check(line, expected, follow_imports=False):
|
||||
script = Script(path=path)
|
||||
@@ -412,3 +427,90 @@ def test_infer_after_parentheses(Script, code, column, expected):
|
||||
assert completions == []
|
||||
else:
|
||||
assert [c.name for c in completions] == [expected]
|
||||
|
||||
|
||||
def test_import_does_not_change_recursion_limit():
|
||||
# Regression test for #1925: importing jedi must not lower a higher
|
||||
# user-defined limit, nor raise a lower one.
|
||||
for limit in (1200, 5000):
|
||||
code = ('import sys; sys.setrecursionlimit(%d); import jedi; '
|
||||
'print(sys.getrecursionlimit())' % limit)
|
||||
output = subprocess.check_output([sys.executable, '-c', code])
|
||||
assert output.strip() == str(limit).encode()
|
||||
|
||||
|
||||
def test_recursion_limit_is_scoped_and_restored():
|
||||
from jedi.api.helpers import higher_recursion_limit, RECURSION_LIMIT
|
||||
|
||||
old_limit = sys.getrecursionlimit()
|
||||
sys.setrecursionlimit(1000)
|
||||
try:
|
||||
with higher_recursion_limit():
|
||||
assert sys.getrecursionlimit() == RECURSION_LIMIT
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
sys.setrecursionlimit(5000)
|
||||
with higher_recursion_limit():
|
||||
assert sys.getrecursionlimit() == 5000
|
||||
assert sys.getrecursionlimit() == 5000
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
|
||||
|
||||
def test_deep_code_works_with_low_recursion_limit(Script):
|
||||
old_limit = sys.getrecursionlimit()
|
||||
sys.setrecursionlimit(1000)
|
||||
try:
|
||||
code = 'x = ' + '(' * 200 + '1' + ')' * 200 + '\nx.'
|
||||
assert Script(code).complete(2, 2)
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', [
|
||||
'infer', 'get_type_hint', 'get_signatures', 'execute', 'defined_names',
|
||||
])
|
||||
def test_name_inference_recursion_limit(Script, method):
|
||||
old_limit = sys.getrecursionlimit()
|
||||
sys.setrecursionlimit(1000)
|
||||
try:
|
||||
name = Script('x = ' + '(' * 200 + '1' + ')' * 200).get_names()[0]
|
||||
getattr(name, method)()
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', ['search', 'complete_search'])
|
||||
def test_project_search_recursion_limit(tmp_path, method):
|
||||
from jedi import Project
|
||||
|
||||
(tmp_path / 'sample.py').write_text('x = ' + '(' * 200 + '1' + ')' * 200)
|
||||
old_limit = sys.getrecursionlimit()
|
||||
sys.setrecursionlimit(1000)
|
||||
try:
|
||||
results = getattr(Project(tmp_path), method)('sample.x.real')
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
assert next(results).name == 'real'
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
results.close()
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
|
||||
|
||||
def test_recursion_limit_restored_after_error():
|
||||
from jedi.api.helpers import recursion_limit
|
||||
|
||||
@recursion_limit
|
||||
def fail():
|
||||
raise ValueError('test')
|
||||
|
||||
old_limit = sys.getrecursionlimit()
|
||||
sys.setrecursionlimit(1000)
|
||||
try:
|
||||
with pytest.raises(ValueError, match='test'):
|
||||
fail()
|
||||
assert sys.getrecursionlimit() == 1000
|
||||
finally:
|
||||
sys.setrecursionlimit(old_limit)
|
||||
@@ -462,3 +462,21 @@ def test_module_completions(Script, module):
|
||||
|
||||
def test_whitespace_at_end_after_dot(Script):
|
||||
assert 'strip' in [c.name for c in Script('str. ').complete()]
|
||||
|
||||
|
||||
def test_param_docstring_in_completion(Script):
|
||||
# From Github #2063
|
||||
code = '''\
|
||||
from typing import Optional
|
||||
|
||||
def func(arg: Optional[int]):
|
||||
pass
|
||||
|
||||
func(arg'''
|
||||
script = Script(code=code)
|
||||
completions = script.complete(
|
||||
line=len(code.splitlines()),
|
||||
column=len(code.splitlines()[-1])
|
||||
)
|
||||
c, = completions
|
||||
assert c.docstring() == ''
|
||||
@@ -53,6 +53,21 @@ def x():
|
||||
]
|
||||
'''
|
||||
|
||||
async_code = '''\
|
||||
async def coro():
|
||||
return None
|
||||
'''
|
||||
async_method = '''\
|
||||
class C:
|
||||
async def coro(self):
|
||||
return None
|
||||
'''
|
||||
async_nested = '''\
|
||||
async def outer():
|
||||
async def inner():
|
||||
return None
|
||||
'''
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'code, line, column, full_name, expected_parents', [
|
||||
@@ -106,6 +121,21 @@ def x():
|
||||
(with_brackets, 3, None, 'myfile', []),
|
||||
(with_brackets, 4, 4, 'myfile.x', ['x']),
|
||||
(with_brackets, 4, 5, 'myfile.x', ['x']),
|
||||
|
||||
(async_code, 2, 0, 'myfile', []),
|
||||
(async_code, 2, 4, 'myfile.coro', ['coro']),
|
||||
(async_code, 2, 5, 'myfile.coro', ['coro']),
|
||||
(async_code, 2, None, 'myfile.coro', ['coro']),
|
||||
('@decorator\n' + async_code, 3, 0, 'myfile', []),
|
||||
('@decorator\n' + async_code, 3, 4, 'myfile.coro', ['coro']),
|
||||
('@decorator\n' + async_code, 3, 5, 'myfile.coro', ['coro']),
|
||||
(async_method, 3, 4, 'myfile.C', ['C']),
|
||||
(async_method, 3, 8, 'myfile.C.coro', ['C', 'coro']),
|
||||
(async_method, 3, 9, 'myfile.C.coro', ['C', 'coro']),
|
||||
(async_nested, 3, 0, 'myfile', []),
|
||||
(async_nested, 3, 4, 'myfile.outer', ['outer']),
|
||||
(async_nested, 3, 8, None, ['outer', 'inner']),
|
||||
(async_nested, 3, 9, None, ['outer', 'inner']),
|
||||
]
|
||||
)
|
||||
def test_context(Script, code, line, column, full_name, expected_parents):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -54,6 +55,24 @@ def test_load_save_project(tmpdir):
|
||||
assert loaded.added_sys_path == ['/foo']
|
||||
|
||||
|
||||
def test_load_project_ignores_environment_path(tmpdir, monkeypatch):
|
||||
# The project file can be part of a checked out repository, so it must not
|
||||
# be able to point Jedi at a binary to execute.
|
||||
def _get_subprocess(self):
|
||||
raise RuntimeError('Should not get called!')
|
||||
|
||||
monkeypatch.setattr('jedi.api.environment.Environment._get_subprocess',
|
||||
_get_subprocess)
|
||||
|
||||
Project(tmpdir.strpath, environment_path=sys.executable).save()
|
||||
assert Project.load(tmpdir.strpath)._environment_path is None
|
||||
|
||||
|
||||
def test_load_project_ignores_unsafe_extensions(tmpdir):
|
||||
Project(tmpdir.strpath, load_unsafe_extensions=True).save()
|
||||
assert Project.load(tmpdir.strpath).load_unsafe_extensions is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'string, full_names, kwargs', [
|
||||
('test_load_save_project', ['test_api.test_project.test_load_save_project'], {}),
|
||||
|
||||
Reference in new issue
Block a user