Compare commits

...
27 Commits
Author SHA1 Message Date
Dave Halter 2af1a0f394 Fix tests 2026-10-13 12:23:01 +02:00
Dave Halter 3fd1c87716 Avoid recursions in signatures, fixes #2085 2026-10-13 11:30:30 +02:00
Dave Halter e7843ba82c Fix a test issue 2026-10-13 09:30:21 +02:00
Dave Halter 5d3295e0f9 Implement pyrepl completions, fixes #2069 2026-10-13 03:13:55 +02:00
Dave Halter 91206f4c65 Allow typing.overload and typing_extensions.overload, fixes #1840, fixes #2117 2026-10-12 23:03:31 +02:00
Dave Halter 8a56168d9a Upgrade zuban 2026-10-12 23:01:49 +02:00
Dave Halter a94b899c13 Avoid an issue around lambdas, fixes #1978 2026-10-12 22:37:24 +02:00
Dave Halter dd7d4a92f4 Avoid a crash, fixes #1990 2026-10-12 22:33:34 +02:00
Dave Halter ea1b96ff10 Add better typing for completions 2026-10-12 21:08:40 +02:00
Dave Halter ab89beb06c Make sure parso is the latest version 2026-10-12 17:54:53 +02:00
Dave Halter b7009b938a Create release for 0.20.1 2026-10-12 17:48:29 +02:00
Dave Halter 142bd1d0be Fix a small typo 2026-10-12 17:40:32 +02:00
Dave Halter 575c6ab378 Don't save properties to project.json that are dropped anyway 2026-10-12 17:34:14 +02:00
Dave Halter c71dead915 Change a bit of typing information 2026-10-12 17:06:43 +02:00
Dave Halter 091735802d Add to the Changelog 2026-10-12 17:01:24 +02:00
Dave Halter 102700116c Fix py__path__ availability in #2118 2026-10-12 16:43:09 +02:00
Dave Halter 79d9764bce Merge pull request #2119 from abdull-ah-med/codex/async-pytest-fixtures
Recognize asynchronous pytest fixtures
2026-10-12 11:05:00 +00:00
Abdullah AhmedandClaude Sonnet 5.5 3f7e8adddc Honor return annotations of async pytest fixtures
Async fixtures skipped the annotation branch, so a `-> T` annotation was
ignored and only the body was inferred. Infer the annotation directly:
the awaited value for coroutines, and the yielded value for annotated
async generators. Add cases where the body is not inferable.

Also drop the tooling note from the AUTHORS entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:52:32 -04:00
Abdullah Ahmed d8efff8e30 Recognize asynchronous pytest fixtures 2026-10-12 04:05:48 -04:00
Dave Halter 2e3e905707 Add to the changelog 2026-10-01 08:42:53 +02:00
Charan Rathore 558135e8f3 Stop changing the global recursion limit on import (#2113) 2026-10-01 06:38:00 +00:00
Dhanu-dynamic f37a9f9feb Ignore environment_path in loaded project files (#2110)
A .jedi/project.json can be part of a checked out repository, so honouring its environment_path let an untrusted file point Jedi at a binary to execute. Rather than trying to gate that binary (the ownership heuristic in _is_unix_safe_simple is meaningless on Windows, where st_uid is always 0), drop environment_path from loaded project files entirely, the same way load_unsafe_extensions is already ignored. It stays available when a Project is constructed directly by the user.
2026-09-29 13:58:51 +00:00
Dave Halter ed47f89162 Fix docstrings for param completions, fixes #2063, fixes #2109 2026-09-27 11:41:13 +02:00
Dhanu-dynamic 112d255414 Do not trust code execution settings from .jedi/project.json (#2108)
get_default_project() loads the first .jedi/project.json above the analysed file, which can be part of a checked out repository. Its environment_path was executed with safe=False and its load_unsafe_extensions was honoured. Loaded projects now check the binary like find_virtualenvs and ignore load_unsafe_extensions.
2026-09-26 12:52:55 +00:00
曾楚笑 5c37b736a3 Fix get_context indentation for async functions (#2107) 2026-09-22 21:50:04 +00:00
Elias 19236b472b Avoid goto crashes on unfinished keyword calls (#2106) 2026-09-20 07:13:11 +00:00
ef5d361121 Fix typos in comments and docstrings (#2101)
ci / tests (3.10, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (3.10, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (3.10, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (3.10, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (3.10, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (3.10, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (3.10, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (3.10, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (3.10, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (3.10, windows-2022, 3.14) (push) Canceled after 0s
ci / tests (3.11, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (3.11, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (3.11, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (3.11, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (3.11, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (3.11, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (3.11, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (3.11, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (3.11, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (3.11, windows-2022, 3.14) (push) Canceled after 0s
ci / tests (3.12, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (3.12, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (3.12, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (3.12, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (3.12, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (3.12, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (3.12, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (3.12, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (3.12, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (3.12, windows-2022, 3.14) (push) Canceled after 0s
ci / tests (3.13, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (3.13, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (3.13, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (3.13, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (3.13, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (3.13, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (3.13, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (3.13, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (3.13, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (3.13, windows-2022, 3.14) (push) Canceled after 0s
ci / tests (3.14, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (3.14, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (3.14, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (3.14, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (3.14, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (3.14, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (3.14, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (3.14, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (3.14, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (3.14, windows-2022, 3.14) (push) Canceled after 0s
ci / tests (interpreter, ubuntu-24.04, 3.10) (push) Canceled after 0s
ci / tests (interpreter, ubuntu-24.04, 3.11) (push) Canceled after 0s
ci / tests (interpreter, ubuntu-24.04, 3.12) (push) Canceled after 0s
ci / tests (interpreter, ubuntu-24.04, 3.13) (push) Canceled after 0s
ci / tests (interpreter, ubuntu-24.04, 3.14) (push) Canceled after 0s
ci / tests (interpreter, windows-2022, 3.10) (push) Canceled after 0s
ci / tests (interpreter, windows-2022, 3.11) (push) Canceled after 0s
ci / tests (interpreter, windows-2022, 3.12) (push) Canceled after 0s
ci / tests (interpreter, windows-2022, 3.13) (push) Canceled after 0s
ci / tests (interpreter, windows-2022, 3.14) (push) Canceled after 0s
ci / code-quality (push) Canceled after 0s
ci / coverage (push) Canceled after 0s
Found by codespell:
- curent -> current (classes.py)
- suppport -> support, propeties -> properties (inference/__init__.py)
- existance -> existence (inference/analysis.py)
- Propably -> Probably (inference/value/instance.py)

Co-authored-by: maxtaran2010 <ocotifuzo727@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 23:42:11 +00:00
33 changed files with 566 additions and 64 deletions

No files matched your search

+5
View File
@@ -66,6 +66,11 @@ Code Contributors
- Martin Vielsmaier (@moser) <martin@vielsmaier.net> - Martin Vielsmaier (@moser) <martin@vielsmaier.net>
- TingJia Wu (@WutingjiaX) <wutingjia@bytedance.com> - TingJia Wu (@WutingjiaX) <wutingjia@bytedance.com>
- Nguyễn Hồng Quân <ng.hong.quan@gmail.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. And a few more "anonymous" contributors.
+8
View File
@@ -6,6 +6,14 @@ Changelog
Unreleased 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
- Work better with PEP 695 type parameter syntax
0.20.0 (2026-05-02) 0.20.0 (2026-05-02)
+++++++++++++++++++ +++++++++++++++++++
+1 -1
View File
@@ -27,7 +27,7 @@ ad
load load
""" """
__version__ = '0.20.0' __version__ = '0.20.1'
from jedi.api import Script, Interpreter, set_debug_function, preload_module from jedi.api import Script, Interpreter, set_debug_function, preload_module
from jedi import settings from jedi import settings
+20 -8
View File
@@ -7,8 +7,8 @@ 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 debug messages to stdout, simply call :func:`set_debug_function` without
arguments. arguments.
""" """
import sys
from pathlib import Path from pathlib import Path
from typing import Any
import parso import parso
from parso.python import tree from parso.python import tree
@@ -21,7 +21,7 @@ from jedi.file_io import KnownContentFileIO
from jedi.api import classes from jedi.api import classes
from jedi.api import interpreter from jedi.api import interpreter
from jedi.api import helpers 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.completion import Completion, search_in_module
from jedi.api.keywords import KeywordName from jedi.api.keywords import KeywordName
from jedi.api.environment import InterpreterEnvironment from jedi.api.environment import InterpreterEnvironment
@@ -43,10 +43,6 @@ from jedi.inference.gradual.conversion import convert_names, convert_values
from jedi.inference.gradual.utils import load_proper_stub_module from jedi.inference.gradual.utils import load_proper_stub_module
from jedi.inference.utils import to_list 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: class Script:
""" """
@@ -97,6 +93,7 @@ class Script:
references works well, because the right folder is searched. There are references works well, because the right folder is searched. There are
also ways to modify the sys path and other things. also ways to modify the sys path and other things.
""" """
@recursion_limit
def __init__(self, code=None, *, path=None, environment=None, project=None): def __init__(self, code=None, *, path=None, environment=None, project=None):
self._orig_path = path self._orig_path = path
if isinstance(path, str): if isinstance(path, str):
@@ -191,8 +188,9 @@ class Script:
self._inference_state.environment, self._inference_state.environment,
) )
@recursion_limit
@validate_line_column @validate_line_column
def complete(self, line=None, column=None, *, fuzzy=False): def complete(self, line=None, column=None, *, fuzzy=False) -> list[classes.Completion]:
""" """
Completes objects under the cursor. Completes objects under the cursor.
@@ -214,6 +212,7 @@ class Script:
) )
return completion.complete() return completion.complete()
@recursion_limit
@validate_line_column @validate_line_column
def infer(self, line=None, column=None, *, only_stubs=False, prefer_stubs=False): def infer(self, line=None, column=None, *, only_stubs=False, prefer_stubs=False):
""" """
@@ -259,6 +258,7 @@ class Script:
# the API. # the API.
return helpers.sorted_definitions(set(defs)) return helpers.sorted_definitions(set(defs))
@recursion_limit
@validate_line_column @validate_line_column
def goto(self, line=None, column=None, *, follow_imports=False, follow_builtin_imports=False, def goto(self, line=None, column=None, *, follow_imports=False, follow_builtin_imports=False,
only_stubs=False, prefer_stubs=False): only_stubs=False, prefer_stubs=False):
@@ -324,6 +324,7 @@ class Script:
""" """
return self._search_func(string, all_scopes=all_scopes) return self._search_func(string, all_scopes=all_scopes)
@recursion_limit
@to_list @to_list
def _search_func(self, string, all_scopes=False, complete=False, fuzzy=False): def _search_func(self, string, all_scopes=False, complete=False, fuzzy=False):
names = self._names(all_scopes=all_scopes) names = self._names(all_scopes=all_scopes)
@@ -338,7 +339,7 @@ class Script:
fuzzy=fuzzy, fuzzy=fuzzy,
) )
def complete_search(self, string, **kwargs): def complete_search(self, string, **kwargs) -> list[Any]:
""" """
Like :meth:`.Script.search`, but completes that string. If you want to Like :meth:`.Script.search`, but completes that string. If you want to
have all possible definitions in a file you can also provide an empty have all possible definitions in a file you can also provide an empty
@@ -353,6 +354,7 @@ class Script:
""" """
return self._search_func(string, complete=True, **kwargs) return self._search_func(string, complete=True, **kwargs)
@recursion_limit
@validate_line_column @validate_line_column
def help(self, line=None, column=None): def help(self, line=None, column=None):
""" """
@@ -397,6 +399,7 @@ class Script:
return [classes.Name(self._inference_state, name)] return [classes.Name(self._inference_state, name)]
return [] return []
@recursion_limit
@validate_line_column @validate_line_column
def get_references(self, line=None, column=None, **kwargs): def get_references(self, line=None, column=None, **kwargs):
""" """
@@ -428,6 +431,7 @@ class Script:
return helpers.sorted_definitions(definitions) return helpers.sorted_definitions(definitions)
return _references(**kwargs) return _references(**kwargs)
@recursion_limit
@validate_line_column @validate_line_column
def get_signatures(self, line=None, column=None): def get_signatures(self, line=None, column=None):
""" """
@@ -466,6 +470,7 @@ class Script:
return [classes.Signature(self._inference_state, signature, call_details) return [classes.Signature(self._inference_state, signature, call_details)
for signature in definitions.get_signatures()] for signature in definitions.get_signatures()]
@recursion_limit
@validate_line_column @validate_line_column
def get_context(self, line=None, column=None): def get_context(self, line=None, column=None):
""" """
@@ -502,6 +507,8 @@ class Script:
tree_name = name.tree_name tree_name = name.tree_name
if tree_name is not None: # Happens with lambdas. if tree_name is not None: # Happens with lambdas.
scope = tree_name.get_definition() scope = tree_name.get_definition()
if scope.parent.type in ('async_stmt', 'async_funcdef'):
scope = scope.parent
if scope.start_pos[1] < column: if scope.start_pos[1] < column:
break break
definition = definition.parent() definition = definition.parent()
@@ -541,6 +548,7 @@ class Script:
finally: finally:
self._inference_state.is_analysis = False self._inference_state.is_analysis = False
@recursion_limit
def get_names(self, **kwargs): def get_names(self, **kwargs):
""" """
Returns names defined in the current file. Returns names defined in the current file.
@@ -579,6 +587,7 @@ class Script:
] ]
return sorted(defs, key=lambda x: x.start_pos) return sorted(defs, key=lambda x: x.start_pos)
@recursion_limit
def rename(self, line=None, column=None, *, new_name): def rename(self, line=None, column=None, *, new_name):
""" """
Renames all references of the variable under the cursor. Renames all references of the variable under the cursor.
@@ -591,6 +600,7 @@ class Script:
definitions = self.get_references(line, column, include_builtins=False) definitions = self.get_references(line, column, include_builtins=False)
return refactoring.rename(self._inference_state, definitions, new_name) return refactoring.rename(self._inference_state, definitions, new_name)
@recursion_limit
@validate_line_column @validate_line_column
def extract_variable(self, line, column, *, new_name, until_line=None, until_column=None): def extract_variable(self, line, column, *, new_name, until_line=None, until_column=None):
""" """
@@ -630,6 +640,7 @@ class Script:
new_name, (line, column), until_pos new_name, (line, column), until_pos
) )
@recursion_limit
@validate_line_column @validate_line_column
def extract_function(self, line, column, *, new_name, until_line=None, until_column=None): def extract_function(self, line, column, *, new_name, until_line=None, until_column=None):
""" """
@@ -677,6 +688,7 @@ class Script:
new_name, (line, column), until_pos new_name, (line, column), until_pos
) )
@recursion_limit
def inline(self, line=None, column=None): def inline(self, line=None, column=None):
""" """
Inlines a variable under the cursor. This is basically the opposite of Inlines a variable under the cursor. This is basically the opposite of
+16 -3
View File
@@ -28,7 +28,7 @@ from jedi.inference.gradual.conversion import convert_names, convert_values
from jedi.inference.base_value import ValueSet, HasNoContext from jedi.inference.base_value import ValueSet, HasNoContext
from jedi.api.keywords import KeywordName from jedi.api.keywords import KeywordName
from jedi.api import completion_cache 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): def _sort_names_by_start_pos(names):
@@ -262,6 +262,7 @@ class BaseName:
return last_leaf.end_pos return last_leaf.end_pos
return definition.end_pos return definition.end_pos
@recursion_limit
def docstring(self, raw=False, fast=True): def docstring(self, raw=False, fast=True):
r""" r"""
Return a document string for this completion object. 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' return tree_name.is_definition() and tree_name.parent.type == 'trailer'
@debug.increase_indent_cm('goto on name') @debug.increase_indent_cm('goto on name')
@recursion_limit
def goto(self, *, follow_imports=False, follow_builtin_imports=False, def goto(self, *, follow_imports=False, follow_builtin_imports=False,
only_stubs=False, prefer_stubs=False): only_stubs=False, prefer_stubs=False):
@@ -452,6 +454,7 @@ class BaseName:
for n in names] for n in names]
@debug.increase_indent_cm('infer on name') @debug.increase_indent_cm('infer on name')
@recursion_limit
def infer(self, *, only_stubs=False, prefer_stubs=False): def infer(self, *, only_stubs=False, prefer_stubs=False):
""" """
Like :meth:`.Script.infer`, it can be useful to understand which type 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]) return ''.join(lines[start_index:index + after + 1])
def _get_signatures(self, for_docstring=False): def _get_signatures(self, for_docstring=False):
if self._name.api_type == 'property': if self._name.api_type in ('property', 'param'):
return [] return []
if for_docstring and self._name.api_type == 'statement' and not self.is_stub(): 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 # 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) names = convert_names([self._name], prefer_stubs=True)
return [sig for name in names for sig in name.infer().get_signatures()] return [sig for name in names for sig in name.infer().get_signatures()]
@recursion_limit
def get_signatures(self): def get_signatures(self):
""" """
Returns all potential signatures for a function or a class. Multiple Returns all potential signatures for a function or a class. Multiple
@@ -579,6 +583,7 @@ class BaseName:
for s in self._get_signatures() for s in self._get_signatures()
] ]
@recursion_limit
def execute(self): def execute(self):
""" """
Uses type inference to "execute" this identifier and returns the 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()) return _values_to_definitions(self._name.infer().execute_with_values())
@recursion_limit
def get_type_hint(self): def get_type_hint(self):
""" """
Returns type hints like ``Iterable[int]`` or ``Union[int, str]``. Returns type hints like ``Iterable[int]`` or ``Union[int, str]``.
@@ -670,6 +676,7 @@ class Completion(BaseName):
""" """
return self._complete(False) return self._complete(False)
@recursion_limit
def docstring(self, raw=False, fast=True): def docstring(self, raw=False, fast=True):
""" """
Documented under :meth:`BaseName.docstring`. Documented under :meth:`BaseName.docstring`.
@@ -752,6 +759,7 @@ class Name(BaseName):
super().__init__(inference_state, definition) super().__init__(inference_state, definition)
@memoize_method @memoize_method
@recursion_limit
def defined_names(self): def defined_names(self):
""" """
List sub-definitions (e.g., methods in class). List sub-definitions (e.g., methods in class).
@@ -797,6 +805,7 @@ class BaseSignature(Name):
self._signature = signature self._signature = signature
@property @property
@recursion_limit
def params(self): def params(self):
""" """
Returns definitions for all parameters that a signature defines. Returns definitions for all parameters that a signature defines.
@@ -807,6 +816,7 @@ class BaseSignature(Name):
return [ParamName(self._inference_state, n) return [ParamName(self._inference_state, n)
for n in self._signature.get_param_names(resolve_stars=True)] for n in self._signature.get_param_names(resolve_stars=True)]
@recursion_limit
def to_string(self): def to_string(self):
""" """
Returns a text representation of the signature. This could for example Returns a text representation of the signature. This could for example
@@ -831,7 +841,7 @@ class Signature(BaseSignature):
def index(self): def index(self):
""" """
Returns the param index of the current cursor position. 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 :rtype: int
""" """
@@ -858,6 +868,7 @@ class Signature(BaseSignature):
class ParamName(Name): class ParamName(Name):
@recursion_limit
def infer_default(self): def infer_default(self):
""" """
Returns default values like the ``1`` of ``def foo(x=1):``. 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()) return _values_to_definitions(self._name.infer_default())
@recursion_limit
def infer_annotation(self, **kwargs): def infer_annotation(self, **kwargs):
""" """
:param execute_annotation: Default True; If False, values are not :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)) return _values_to_definitions(self._name.infer_annotation(ignore_stars=True, **kwargs))
@recursion_limit
def to_string(self): def to_string(self):
""" """
Returns a simple representation of a param, like Returns a simple representation of a param, like
+51 -4
View File
@@ -2,11 +2,14 @@
Helpers for the API Helpers for the API
""" """
import re import re
import sys
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager
from textwrap import dedent from textwrap import dedent
from itertools import chain from itertools import chain
from functools import wraps from functools import wraps
from inspect import Parameter from inspect import Parameter
from typing import Callable, TypeVar, Any, ParamSpec
from parso.python.parser import Parser from parso.python.parser import Parser
from parso.python import tree from parso.python import tree
@@ -18,9 +21,53 @@ from jedi.inference.compiled import get_string_value_set
from jedi.cache import signature_time_cache, memoize_method from jedi.cache import signature_time_cache, memoize_method
from jedi.parser_utils import get_parent_scope from jedi.parser_utils import get_parent_scope
T = TypeVar("T")
P = ParamSpec("P")
CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name']) 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: Callable[P, T]) -> Callable[P, T]:
@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): def _start_match(string, like_name):
return string.startswith(like_name) return string.startswith(like_name)
@@ -465,9 +512,9 @@ def cache_signatures(inference_state, context, bracket_leaf, code_lines, user_po
) )
def validate_line_column(func): def validate_line_column(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func) @wraps(func)
def wrapper(self, line=None, column=None, *args, **kwargs): def wrapper(self, line=None, column=None, *args, **kwargs) -> Any:
line = max(len(self._code_lines), 1) if line is None else line line = max(len(self._code_lines), 1) if line is None else line
if not (0 < line <= len(self._code_lines)): if not (0 < line <= len(self._code_lines)):
raise ValueError('`line` parameter is not in a valid range.') raise ValueError('`line` parameter is not in a valid range.')
@@ -484,8 +531,8 @@ def validate_line_column(func):
raise ValueError('`column` parameter (%d) is not in a valid range ' raise ValueError('`column` parameter (%d) is not in a valid range '
'(0-%d) for line %d (%r).' % ( '(0-%d) for line %d (%r).' % (
column, line_len, line, line_string)) column, line_len, line, line_string))
return func(self, line, column, *args, **kwargs) return func(self, line, column, *args, **kwargs) # type: ignore[arg-type]
return wrapper return wrapper # type: ignore[return-value]
def get_module_names(module, all_scopes, definitions=True, references=False): def get_module_names(module, all_scopes, definitions=True, references=False):
+19 -2
View File
@@ -15,7 +15,7 @@ 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
from jedi.api.completion import search_in_module 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, \ from jedi.inference.imports import load_module_from_path, \
load_namespace_from_path, iter_module_names load_namespace_from_path, iter_module_names
from jedi.inference.sys_path import discover_buildout_paths from jedi.inference.sys_path import discover_buildout_paths
@@ -86,6 +86,14 @@ class Project:
version, data = json.load(f) version, data = json.load(f)
if version == 1: 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) return cls(**data)
else: else:
raise WrongVersion( raise WrongVersion(
@@ -99,6 +107,11 @@ class Project:
data = dict(self.__dict__) data = dict(self.__dict__)
data.pop('_environment', None) data.pop('_environment', None)
data.pop('_django', None) # TODO make django setting public? 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 = {k.lstrip('_'): v for k, v in data.items()}
data['path'] = str(data['path']) data['path'] = str(data['path'])
@@ -242,7 +255,10 @@ class Project:
def get_environment(self): def get_environment(self):
if self._environment is None: if self._environment is None:
if self._environment_path is not 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: else:
self._environment = get_cached_default_environment() self._environment = get_cached_default_environment()
return self._environment return self._environment
@@ -282,6 +298,7 @@ class Project:
""" """
return self._search_func(string, complete=True, **kwargs) return self._search_func(string, complete=True, **kwargs)
@recursion_limit_generator
@_try_to_skip_duplicates @_try_to_skip_duplicates
def _search_func(self, string, complete=False, all_scopes=False): def _search_func(self, string, complete=False, all_scopes=False):
# Using a Script is they easiest way to get an empty module context. # Using a Script is they easiest way to get an empty module context.
+3 -3
View File
@@ -1,8 +1,8 @@
""" """
To use Jedi completion in Python interpreter, add the following in your shell To use Jedi completion in Python interpreter, add the following in your shell
setup (e.g., ``.bashrc``). This works only on Linux/Mac, because readline is setup (e.g., ``.bashrc``). Jedi configures the classic readline shell and
not available on Windows. If you still want Jedi autocompletion in your REPL, Python 3.13+'s new ``_pyrepl`` shell when those hooks are available.
just use IPython instead:: You might also want to try IPython instead, which also uses Jedi::
export PYTHONSTARTUP="$(python -m jedi repl)" export PYTHONSTARTUP="$(python -m jedi repl)"
+1 -1
View File
@@ -125,7 +125,7 @@ class InferenceState:
debug.dbg('execute result: %s in %s', value_set, value) debug.dbg('execute result: %s in %s', value_set, value)
return value_set 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 @property
@inference_state_function_cache() @inference_state_function_cache()
def builtins_module(self): def builtins_module(self):
+1 -1
View File
@@ -110,7 +110,7 @@ def _check_for_setattr(instance):
def add_attribute_error(name_context, lookup_value, name): def add_attribute_error(name_context, lookup_value, name):
message = ('AttributeError: %s has no attribute %s.' % (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. # instead of an error, if that happens.
typ = Error typ = Error
if lookup_value.is_instance() and not lookup_value.is_compiled(): if lookup_value.is_instance() and not lookup_value.is_compiled():
+1 -1
View File
@@ -216,7 +216,7 @@ class Value(HelperValueMixin):
def is_namespace(self): def is_namespace(self):
return False return False
def is_compiled(self): def is_compiled(self) -> bool:
return False return False
def is_bound_method(self): def is_bound_method(self):
+2 -2
View File
@@ -9,7 +9,7 @@ import re
import builtins import builtins
import typing import typing
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple, Any
from jedi.inference.compiled.getattr_static import getattr_static from jedi.inference.compiled.getattr_static import getattr_static
@@ -46,7 +46,7 @@ ClassMethodDescriptorType = type(object_class_dict['__subclasshook__'])
_sentinel = object() _sentinel = object()
# Maps Python syntax to the operator module. # Maps Python syntax to the operator module.
COMPARISON_OPERATORS = { COMPARISON_OPERATORS: dict[str, Any] = {
'==': op.eq, '==': op.eq,
'!=': op.ne, '!=': op.ne,
'is': op.is_, 'is': op.is_,
+2 -3
View File
@@ -256,12 +256,11 @@ class CompiledValue(Value):
arg.execute_annotation(context) arg.execute_annotation(context)
for arg in arguments) for arg in arguments)
elif name: elif name:
# While with_generics only exists on very specific objects, we # These are typically typing module objects. Try to use that to create generics.
# should probably be fine, because we control all the typing
# objects.
return ValueSet([ return ValueSet([
v.with_generics(arguments) v.with_generics(arguments)
for v in self.inference_state.typing_module.py__getattribute__(name) for v in self.inference_state.typing_module.py__getattribute__(name)
if hasattr(v, "with_generics")
]).execute_annotation(context) ]).execute_annotation(context)
return super().execute_annotation(context) return super().execute_annotation(context)
+9 -1
View File
@@ -416,7 +416,15 @@ def import_module(inference_state, import_names, parent_module_value, sys_path):
if is_pkg is None: if is_pkg is None:
return NO_VALUES return NO_VALUES
else: else:
paths = parent_module_value.py__path__() 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: if paths is None:
# The module might not be a package. # The module might not be a package.
return NO_VALUES return NO_VALUES
+2
View File
@@ -170,6 +170,8 @@ class AbstractTreeName(AbstractNameDefinition):
trailer = par.parent trailer = par.parent
if trailer.type == 'arglist': if trailer.type == 'arglist':
trailer = trailer.parent trailer = trailer.parent
if trailer.type == 'error_node':
return []
if trailer.type != 'classdef': if trailer.type != 'classdef':
if trailer.type == 'decorator': if trailer.type == 'decorator':
value_set = context.infer_node(trailer.children[1]) value_set = context.infer_node(trailer.children[1])
+1 -1
View File
@@ -254,4 +254,4 @@ def _error_argument_count(funcdef, actual_count):
else: else:
before = 'from %s to ' % (len(params) - default_arguments) before = 'from %s to ' % (len(params) - default_arguments)
return ('TypeError: %s() takes %s%s arguments (%s given).' return ('TypeError: %s() takes %s%s arguments (%s given).'
% (funcdef.name, before, len(params), actual_count)) % (getattr(funcdef, "name", "<lambda>"), before, len(params), actual_count))
+29 -13
View File
@@ -11,9 +11,10 @@ This means for example in this case::
The signature here for bar should be `bar(b, c)` instead of bar(*args). The signature here for bar should be `bar(b, c)` instead of bar(*args).
""" """
from inspect import Parameter from inspect import Parameter
from typing import Sequence
from jedi.inference.utils import to_list from jedi.inference.utils import to_list
from jedi.inference.names import ParamNameWrapper from jedi.inference.names import ParamNameWrapper, AnonymousParamName
from jedi.inference.helpers import is_big_annoying_library from jedi.inference.helpers import is_big_annoying_library
@@ -93,8 +94,24 @@ def _remove_given_params(arguments, param_names):
yield p yield p
class ParamNameFixedKind(ParamNameWrapper):
def __init__(self, param_name, new_kind):
super().__init__(param_name)
self._new_kind = new_kind
def get_kind(self):
return self._new_kind
@to_list @to_list
def process_params(param_names, star_count=3): # default means both * and ** def process_params(
param_names: Sequence[AnonymousParamName | ParamNameFixedKind],
star_count=3,
already_used=None):
# start_count=3 means both * and **
if already_used is None:
already_used = set()
if param_names: if param_names:
if is_big_annoying_library(param_names[0].parent_context): if is_big_annoying_library(param_names[0].parent_context):
# At first this feature can look innocent, but it does a lot of # At first this feature can look innocent, but it does a lot of
@@ -112,13 +129,21 @@ def process_params(param_names, star_count=3): # default means both * and **
original_arg_name = None original_arg_name = None
original_kwarg_name = None original_kwarg_name = None
for p in param_names: for p in param_names:
def is_recursion():
is_recursion = p.tree_name in already_used
if not is_recursion:
already_used.add(p.tree_name)
return is_recursion
kind = p.get_kind() kind = p.get_kind()
if kind == Parameter.VAR_POSITIONAL: if kind == Parameter.VAR_POSITIONAL:
if star_count & 1: if star_count & 1:
if not is_recursion():
arg_callables = _iter_nodes_for_param(p) arg_callables = _iter_nodes_for_param(p)
original_arg_name = p original_arg_name = p
elif p.get_kind() == Parameter.VAR_KEYWORD: elif p.get_kind() == Parameter.VAR_KEYWORD:
if star_count & 2: if star_count & 2:
if not is_recursion():
kwarg_callables = list(_iter_nodes_for_param(p)) kwarg_callables = list(_iter_nodes_for_param(p))
original_kwarg_name = p original_kwarg_name = p
elif kind == Parameter.KEYWORD_ONLY: elif kind == Parameter.KEYWORD_ONLY:
@@ -157,7 +182,7 @@ def process_params(param_names, star_count=3): # default means both * and **
list(_remove_given_params( list(_remove_given_params(
arguments, arguments,
signature.get_param_names(resolve_stars=False) signature.get_param_names(resolve_stars=False)
)), new_star_count): )), new_star_count, already_used):
if p.get_kind() == Parameter.VAR_KEYWORD: if p.get_kind() == Parameter.VAR_KEYWORD:
kwarg_names.append(p) kwarg_names.append(p)
elif p.get_kind() == Parameter.VAR_POSITIONAL: elif p.get_kind() == Parameter.VAR_POSITIONAL:
@@ -190,7 +215,7 @@ def process_params(param_names, star_count=3): # default means both * and **
list(_remove_given_params( list(_remove_given_params(
arguments, arguments,
signature.get_param_names(resolve_stars=False) signature.get_param_names(resolve_stars=False)
)), star_count=2): )), star_count=2, already_used=already_used):
if p.get_kind() == Parameter.VAR_KEYWORD: if p.get_kind() == Parameter.VAR_KEYWORD:
kwarg_names.append(p) kwarg_names.append(p)
elif p.get_kind() == Parameter.KEYWORD_ONLY: elif p.get_kind() == Parameter.KEYWORD_ONLY:
@@ -206,12 +231,3 @@ def process_params(param_names, star_count=3): # default means both * and **
yield original_kwarg_name yield original_kwarg_name
elif kwarg_names: elif kwarg_names:
yield kwarg_names[0] yield kwarg_names[0]
class ParamNameFixedKind(ParamNameWrapper):
def __init__(self, param_name, new_kind):
super().__init__(param_name)
self._new_kind = new_kind
def get_kind(self):
return self._new_kind
+5 -1
View File
@@ -2,6 +2,7 @@
import functools import functools
import re import re
import os import os
from typing import ParamSpec, TypeVar, Callable, Iterable
_sep = os.path.sep _sep = os.path.sep
@@ -10,8 +11,11 @@ if os.path.altsep is not None:
_path_re = re.compile(r'(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) _path_re = re.compile(r'(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep)))
del _sep del _sep
P = ParamSpec("P")
T = TypeVar("T")
def to_list(func):
def to_list(func: Callable[P, Iterable[T]]) -> Callable[P, list[T]]:
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return list(func(*args, **kwargs)) return list(func(*args, **kwargs))
return wrapper return wrapper
+4
View File
@@ -435,6 +435,10 @@ def _find_overload_functions(context, tree_node):
if dotted_name.type == 'name' and dotted_name.value == 'overload': if dotted_name.type == 'name' and dotted_name.value == 'overload':
# TODO check with values if it's the right overload # TODO check with values if it's the right overload
return True return True
if dotted_name.type == 'atom_expr':
trailer = dotted_name.children[-1]
if trailer.children[0] == "." and trailer.children[1].value == "overload":
return True
return False return False
if tree_node.type == 'lambdef': if tree_node.type == 'lambdef':
+1 -1
View File
@@ -224,7 +224,7 @@ class _BaseTreeInstance(AbstractInstanceValue):
elif isinstance(f, CompiledValueFilter): elif isinstance(f, CompiledValueFilter):
yield CompiledInstanceClassFilter(self, f) yield CompiledInstanceClassFilter(self, f)
else: else:
# Propably from the metaclass. # Probably from the metaclass.
yield f yield f
@inference_state_method_cache() @inference_state_method_cache()
+17
View File
@@ -2,7 +2,9 @@ import sys
from typing import List from typing import List
from pathlib import Path from pathlib import Path
from jedi.inference.arguments import ValuesArguments
from jedi.inference.cache import inference_state_method_cache 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.imports import goto_import, load_module_from_path
from jedi.inference.filters import ParserTreeFilter from jedi.inference.filters import ParserTreeFilter
from jedi.inference.base_value import NO_VALUES, ValueSet from jedi.inference.base_value import NO_VALUES, ValueSet
@@ -32,6 +34,19 @@ def execute(callback):
def infer_anonymous_param(func): def infer_anonymous_param(func):
def get_returns(value): def get_returns(value):
if value.tree_node.annotation is not None: 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() result = value.execute_with_values()
if any(v.name.get_qualified_names(include_module_names=True) if any(v.name.get_qualified_names(include_module_names=True)
== ('typing', 'Generator') == ('typing', 'Generator')
@@ -223,6 +238,8 @@ class FixtureFilter(ParserTreeFilter):
if funcdef.type != "funcdef": if funcdef.type != "funcdef":
return False return False
decorated = funcdef.parent decorated = funcdef.parent
if decorated.type == "async_funcdef":
decorated = decorated.parent
if decorated.type != "decorated": if decorated.type != "decorated":
return False return False
decorators = decorated.children[0] decorators = decorated.children[0]
+41 -11
View File
@@ -100,17 +100,10 @@ def setup_readline(namespace_module=__main__, fuzzy=False):
except IndexError: except IndexError:
return None return None
try: jedi_rl = JediRL()
# Need to import this one as well to make sure it's executed before
# this code. This didn't use to be an issue until 3.3. Starting with def configure_readline(readline):
# 3.4 this is different, it always overwrites the completer if it's not readline.set_completer(jedi_rl.complete)
# already imported here.
import rlcompleter # noqa: F401
import readline
except ImportError:
print("Jedi: Module readline not available.")
else:
readline.set_completer(JediRL().complete)
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
# jedi itself does the case matching # jedi itself does the case matching
readline.parse_and_bind("set completion-ignore-case on") readline.parse_and_bind("set completion-ignore-case on")
@@ -122,6 +115,43 @@ def setup_readline(namespace_module=__main__, fuzzy=False):
# No delimiters, Jedi handles that. # No delimiters, Jedi handles that.
readline.set_completer_delims('') readline.set_completer_delims('')
try:
import _pyrepl.readline as pyrepl_readline # type: ignore
except ImportError:
pyrepl_readline = None
else:
original_setup = getattr(pyrepl_readline, '_setup', None)
if original_setup is not None and not getattr(original_setup, '__jedi_wrapped__', False):
def _jedi_pyrepl_setup(namespace):
original_setup(namespace)
configure_readline(pyrepl_readline)
wrapper = getattr(pyrepl_readline, '_wrapper', None)
if wrapper is not None:
# Reset the original completer to None so Jedi's readline
# completer is preferred.
wrapper.config.module_completer = _NoPyreplModuleCompleter()
_jedi_pyrepl_setup.__jedi_wrapped__ = True # type: ignore[attr-defined]
pyrepl_readline._setup = _jedi_pyrepl_setup
try:
# Need to import this one as well to make sure it's executed before
# this code. This didn't use to be an issue until 3.3. Starting with
# 3.4 this is different, it always overwrites the completer if it's not
# already imported here.
import rlcompleter # noqa: F401
import readline
except ImportError:
if pyrepl_readline is None:
print("Jedi: Module readline not available.")
else:
configure_readline(readline)
class _NoPyreplModuleCompleter:
def get_completions(self, line, include_values=False):
return None
def version_info(): def version_info():
""" """
+2 -2
View File
@@ -37,7 +37,7 @@ setup(name='jedi',
packages=find_packages(exclude=['test', 'test.*']), packages=find_packages(exclude=['test', 'test.*']),
python_requires='>=3.10', python_requires='>=3.10',
# Python 3.13 grammars are added to parso in 0.8.4 # Python 3.13 grammars are added to parso in 0.8.4
install_requires=['parso>=0.8.6,<0.9.0'], install_requires=['parso>=0.8.7,<0.9.0'],
extras_require={ extras_require={
'dev': [ 'dev': [
'pytest<9.0.0', 'pytest<9.0.0',
@@ -50,7 +50,7 @@ setup(name='jedi',
'typing_extensions', 'typing_extensions',
# latest version on 2025-06-16 # latest version on 2025-06-16
'flake8==7.1.2', 'flake8==7.1.2',
'zuban==0.7.0', 'zuban==0.10.1',
# Arbitrary pins, latest at the time of pinning # Arbitrary pins, latest at the time of pinning
'types-setuptools==80.9.0.20250529', 'types-setuptools==80.9.0.20250529',
], ],
+25
View File
@@ -86,3 +86,28 @@ def broken_f1(value: Tuple[T]) -> Tuple[T]: ...
tup: Tuple[float] tup: Tuple[float]
#? float() #? float()
broken_f1(broken_f1(tup))[0] broken_f1(broken_f1(tup))[0]
# -------------------------
# Typing Extensions
# -------------------------
import typing_extensions
import typing
@typing_extensions.overload
def extensions(x: int) -> int: ...
@typing_extensions.overload
def extensions(x: str) -> str: ...
#? int()
extensions(1)
#? str()
extensions("")
@typing.overload
def typing_direct(x: int) -> int: ...
@typing.overload
def typing_direct(x: str) -> str: ...
#? int()
typing_direct(1)
#? str()
typing_direct("")
+56 -1
View File
@@ -1,4 +1,4 @@
from typing import Generator from typing import AsyncGenerator, Generator
import pytest import pytest
from pytest import fixture from pytest import fixture
@@ -208,3 +208,58 @@ def bla(admin_u
def bla(admin_user): def bla(admin_user):
pass 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
+102
View File
@@ -3,6 +3,8 @@ Test all things related to the ``jedi.api`` module.
""" """
import os import os
import subprocess
import sys
from textwrap import dedent from textwrap import dedent
import pytest import pytest
@@ -231,6 +233,19 @@ def test_goto_follow_imports(Script):
assert d.name == 'a' 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 test_goto_module(Script):
def check(line, expected, follow_imports=False): def check(line, expected, follow_imports=False):
script = Script(path=path) script = Script(path=path)
@@ -412,3 +427,90 @@ def test_infer_after_parentheses(Script, code, column, expected):
assert completions == [] assert completions == []
else: else:
assert [c.name for c in completions] == [expected] 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)
+18
View File
@@ -462,3 +462,21 @@ def test_module_completions(Script, module):
def test_whitespace_at_end_after_dot(Script): def test_whitespace_at_end_after_dot(Script):
assert 'strip' in [c.name for c in Script('str. ').complete()] 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() == ''
+30
View File
@@ -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( @pytest.mark.parametrize(
'code, line, column, full_name, expected_parents', [ 'code, line, column, full_name, expected_parents', [
@@ -106,6 +121,21 @@ def x():
(with_brackets, 3, None, 'myfile', []), (with_brackets, 3, None, 'myfile', []),
(with_brackets, 4, 4, 'myfile.x', ['x']), (with_brackets, 4, 4, 'myfile.x', ['x']),
(with_brackets, 4, 5, '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): def test_context(Script, code, line, column, full_name, expected_parents):
+17
View File
@@ -873,3 +873,20 @@ def test_whitespace_after_dot_completion(Script):
# From #1954 # From #1954
completions = jedi.Interpreter("object. \n", []).complete(1, 8) completions = jedi.Interpreter("object. \n", []).complete(1, 8)
assert "mro" in [c.name for c in completions] assert "mro" in [c.name for c in completions]
def test_interpreter_with_literals_no_crash():
# From #1990
class A:
@property
def value(self) -> typing.Literal["a"]:
return "a"
a = A()
interpreter = jedi.Interpreter("a.", [{"a": a}])
value = next(filter(lambda c: c.name == "value", interpreter.complete()))
assert value.name == "value"
interpreter = jedi.Interpreter("a.value.", [{"a": a}])
assert "upper" in [c.name for c in interpreter.complete()]
+19
View File
@@ -1,4 +1,5 @@
import os import os
import sys
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -54,6 +55,24 @@ def test_load_save_project(tmpdir):
assert loaded.added_sys_path == ['/foo'] 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( @pytest.mark.parametrize(
'string, full_names, kwargs', [ 'string, full_names, kwargs', [
('test_load_save_project', ['test_api.test_project.test_load_save_project'], {}), ('test_load_save_project', ['test_api.test_project.test_load_save_project'], {}),
+27
View File
@@ -1,3 +1,5 @@
from textwrap import dedent
import pytest import pytest
_tuple_code = 'from typing import Tuple\ndef f(x: Tuple[int]): ...\nf' _tuple_code = 'from typing import Tuple\ndef f(x: Tuple[int]): ...\nf'
@@ -69,3 +71,28 @@ def test_param_kind_and_name(code, index, param_code, kind, Script):
def test_staticmethod(Script): def test_staticmethod(Script):
s, = Script('staticmethod(').get_signatures() s, = Script('staticmethod(').get_signatures()
assert s.to_string() == 'staticmethod(f: Callable[_P, _R_co], /)' assert s.to_string() == 'staticmethod(f: Callable[_P, _R_co], /)'
def test_recursive_star_kwargs_do_not_recurse(Script):
# From #2085
code = dedent('''
def test(x, **kwargs):
if isinstance(x, int):
return test((x,), **kwargs)
test(
''')
script = Script(code)
script.complete()
signature, = script.get_signatures()
assert signature.to_string() == 'test(x, **kwargs)'
code = dedent('''
def test(x, *args, **kwargs):
if isinstance(x, int):
return test((x,), *args, **kwargs)
test(
''')
script = Script(code)
script.complete()
signature, = script.get_signatures()
assert signature.to_string() == 'test(x, *args, **kwargs)'
+4 -4
View File
@@ -165,7 +165,7 @@ def test_tree_signature(Script, environment, code, expected):
('two_kwargs_redirects(kw, kw)', '*args, a, b, c, **kwargs'), ('two_kwargs_redirects(kw, kw)', '*args, a, b, c, **kwargs'),
('two_kwargs_redirects(simple, kw)', '*args, a, b, c, **kwargs'), ('two_kwargs_redirects(simple, kw)', '*args, a, b, c, **kwargs'),
('two_kwargs_redirects(simple2, two_kwargs_redirects(simple, simple))', ('two_kwargs_redirects(simple2, two_kwargs_redirects(simple, simple))',
'*args, x, a, b, c'), '*args, x, **kwargs'),
('combined_redirect(simple, simple2)', 'a, b, /, *, x'), ('combined_redirect(simple, simple2)', 'a, b, /, *, x'),
('combined_redirect(simple, simple3)', 'a, b, /, *, a, x: int'), ('combined_redirect(simple, simple3)', 'a, b, /, *, a, x: int'),
@@ -180,11 +180,11 @@ def test_tree_signature(Script, environment, code, expected):
('combined_lot_of_args(simple4, kw)', '*, b, c, **kwargs'), ('combined_lot_of_args(simple4, kw)', '*, b, c, **kwargs'),
('combined_redirect(combined_redirect(simple2, simple4), combined_redirect(kw, simple5))', ('combined_redirect(combined_redirect(simple2, simple4), combined_redirect(kw, simple5))',
'x, /, *, y'), '*args, **kwargs'),
('combined_redirect(combined_redirect(simple4, simple2), combined_redirect(simple5, kw))', ('combined_redirect(combined_redirect(simple4, simple2), combined_redirect(simple5, kw))',
'a, b, x: int, /, *, a, b, c, **kwargs'), '*args, **kwargs'),
('combined_redirect(combined_redirect(a, kw), combined_redirect(kw, simple5))', ('combined_redirect(combined_redirect(a, kw), combined_redirect(kw, simple5))',
'a, b, /, *args, y'), '*args, **kwargs'),
('no_redirect(kw)', '*args, **kwargs'), ('no_redirect(kw)', '*args, **kwargs'),
('no_redirect(akw)', '*args, **kwargs'), ('no_redirect(akw)', '*args, **kwargs'),
+27
View File
@@ -1,4 +1,5 @@
import sys import sys
import types
from typing import Any from typing import Any
try: try:
@@ -136,3 +137,29 @@ class TestSetupReadline(unittest.TestCase):
def test_version_info(): def test_version_info():
assert utils.version_info()[:2] > (0, 7) assert utils.version_info()[:2] > (0, 7)
def test_pyrepl():
try:
from _pyrepl import readline # zuban: ignore[import-not-found]
except ImportError:
return # This is typical before Python 3.13
utils.setup_readline(types.SimpleNamespace())
readline._setup({})
def complete(text):
completer = readline.get_completer()
i = 0
completions = []
while True:
completion = completer(text, i)
if completion is None:
break
completions.append(completion)
i += 1
return completions
assert complete("str") == ["str"]
assert complete("str().upp") == ["str().upper"]
assert complete("import multiproc") == ["import multiprocessing"]