Compare commits

..
6 Commits
Author SHA1 Message Date
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)
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
12 changed files with 104 additions and 6 deletions
+3
View File
@@ -66,6 +66,9 @@ 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>
And a few more "anonymous" contributors. And a few more "anonymous" contributors.
+2
View File
@@ -502,6 +502,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()
+2 -2
View File
@@ -551,7 +551,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
@@ -831,7 +831,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
""" """
+12 -1
View File
@@ -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(
@@ -242,7 +250,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
+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():
+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
@@ -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()
+13
View File
@@ -231,6 +231,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)
+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):
+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'], {}),