From 30ef824abd506f87e7413193384da48a18920b7c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 4 Feb 2026 01:19:14 +0100 Subject: [PATCH 01/18] Introduce some stricter typing --- jedi/api/environment.py | 50 ++++++++++--------- jedi/api/keywords.py | 2 +- jedi/file_io.py | 3 ++ jedi/inference/__init__.py | 3 ++ jedi/inference/arguments.py | 3 ++ jedi/inference/base_value.py | 16 +++++- .../inference/compiled/subprocess/__init__.py | 6 ++- jedi/inference/compiled/value.py | 4 +- jedi/inference/context.py | 12 ++++- jedi/inference/docstring_utils.py | 2 +- jedi/inference/filters.py | 9 ++-- jedi/inference/gradual/generics.py | 9 ++++ jedi/inference/gradual/typing.py | 3 ++ jedi/inference/helpers.py | 2 +- jedi/inference/imports.py | 2 +- jedi/inference/names.py | 25 +++++++--- jedi/inference/signature.py | 7 +++ jedi/inference/syntax_tree.py | 4 +- jedi/inference/utils.py | 1 - jedi/inference/value/function.py | 6 +++ jedi/inference/value/instance.py | 3 ++ jedi/inference/value/iterable.py | 16 ++++++ jedi/inference/value/klass.py | 14 +++++- jedi/inference/value/module.py | 14 +++++- jedi/parser_utils.py | 2 +- jedi/plugins/pytest.py | 2 +- jedi/plugins/stdlib.py | 2 +- pyproject.toml | 16 +----- 28 files changed, 171 insertions(+), 67 deletions(-) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index d2f7bd29..0bf9169e 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -8,7 +8,7 @@ import hashlib import filecmp from collections import namedtuple from shutil import which -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from jedi.cache import memoize_method, time_cache from jedi.inference.compiled.subprocess import CompiledSubprocess, \ @@ -36,6 +36,9 @@ class InvalidPythonEnvironment(Exception): class _BaseEnvironment: + version_info: Any + executable: Any + @memoize_method def get_grammar(self): version_string = '%s.%s' % (self.version_info.major, self.version_info.minor) @@ -355,7 +358,7 @@ def get_system_environment(version, *, env_vars=None): return SameEnvironment() return Environment(exe) - if os.name == 'nt': + if sys.platform == "win32": for exe in _get_executables_from_windows_registry(version): try: return Environment(exe, env_vars=env_vars) @@ -383,7 +386,7 @@ def _get_executable_path(path, safe=True): Returns None if it's not actually a virtual env. """ - if os.name == 'nt': + if sys.platform == "win32": pythons = [os.path.join(path, 'Scripts', 'python.exe'), os.path.join(path, 'python.exe')] else: pythons = [os.path.join(path, 'bin', 'python')] @@ -397,27 +400,28 @@ def _get_executable_path(path, safe=True): return python -def _get_executables_from_windows_registry(version): - import winreg +if sys.platform == "win32": + def _get_executables_from_windows_registry(version): + import winreg - # TODO: support Python Anaconda. - sub_keys = [ - r'SOFTWARE\Python\PythonCore\{version}\InstallPath', - r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath', - r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath', - r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath' - ] - for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]: - for sub_key in sub_keys: - sub_key = sub_key.format(version=version) - try: - with winreg.OpenKey(root_key, sub_key) as key: - prefix = winreg.QueryValueEx(key, '')[0] - exe = os.path.join(prefix, 'python.exe') - if os.path.isfile(exe): - yield exe - except WindowsError: - pass + # TODO: support Python Anaconda. + sub_keys = [ + r'SOFTWARE\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath' + ] + for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]: + for sub_key in sub_keys: + sub_key = sub_key.format(version=version) + try: + with winreg.OpenKey(root_key, sub_key) as key: + prefix = winreg.QueryValueEx(key, '')[0] + exe = os.path.join(prefix, 'python.exe') + if os.path.isfile(exe): + yield exe + except WindowsError: + pass def _assert_safe(executable_path, safe): diff --git a/jedi/api/keywords.py b/jedi/api/keywords.py index 80ff13c3..207daef8 100644 --- a/jedi/api/keywords.py +++ b/jedi/api/keywords.py @@ -41,7 +41,7 @@ def imitate_pydoc(string): try: # is a tuple now - label, related = string + label, related = string # type: ignore[misc] except TypeError: return '' diff --git a/jedi/file_io.py b/jedi/file_io.py index ead17335..a9a0b13e 100644 --- a/jedi/file_io.py +++ b/jedi/file_io.py @@ -1,4 +1,5 @@ import os +from typing import Any from parso import file_io @@ -58,6 +59,8 @@ class FolderIO(AbstractFolderIO): class FileIOFolderMixin: + path: Any + def get_parent_folder(self): return FolderIO(os.path.dirname(self.path)) diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index 8e1696e2..2d0a7099 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -62,6 +62,8 @@ I need to mention now that lazy type inference is really good because it only *inferes* what needs to be *inferred*. All the statements and modules that are not used are just being ignored. """ +from typing import Any + import parso from jedi.file_io import FileIO @@ -82,6 +84,7 @@ from jedi.plugins import plugin_manager class InferenceState: + analysis_modules: list[Any] def __init__(self, project, environment=None, script_path=None): if environment is None: environment = project.get_environment() diff --git a/jedi/inference/arguments.py b/jedi/inference/arguments.py index 8602f494..b2c28ad3 100644 --- a/jedi/inference/arguments.py +++ b/jedi/inference/arguments.py @@ -1,5 +1,6 @@ import re from itertools import zip_longest +from typing import Any from parso.python import tree @@ -164,6 +165,8 @@ def unpack_arglist(arglist): class TreeArguments(AbstractArguments): + context: Any + def __init__(self, inference_state, context, argument_node, trailer=None): """ :param argument_node: May be an argument_node or a list of nodes. diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 9a789a4e..6df71e9f 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -9,6 +9,7 @@ just one. from functools import reduce from operator import add from itertools import zip_longest +from typing import TYPE_CHECKING, Any from parso.python.tree import Name @@ -21,12 +22,25 @@ from jedi.cache import memoize_method sentinel = object() +if TYPE_CHECKING: + from jedi.inference import InferenceState + class HasNoContext(Exception): pass class HelperValueMixin: + parent_context: Any + inference_state: "InferenceState" + name: Any + get_filters: Any + is_stub: Any + py__getattribute__alternatives: Any + py__iter__: Any + py__mro__: Any + _as_context: Any + def get_root_context(self): value = self if value.parent_context is None: @@ -499,7 +513,7 @@ class ValueSet: return ValueSet.from_sets(_getitem(c, *args, **kwargs) for c in self._set) def try_merge(self, function_name): - value_set = self.__class__([]) + value_set = ValueSet([]) for c in self._set: try: method = getattr(c, function_name) diff --git a/jedi/inference/compiled/subprocess/__init__.py b/jedi/inference/compiled/subprocess/__init__.py index 3a6039f7..5f18e20f 100644 --- a/jedi/inference/compiled/subprocess/__init__.py +++ b/jedi/inference/compiled/subprocess/__init__.py @@ -33,7 +33,7 @@ import traceback import weakref from functools import partial from threading import Thread -from typing import Dict, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING, Any from jedi._compatibility import pickle_dump, pickle_load from jedi import debug @@ -52,7 +52,7 @@ PICKLE_PROTOCOL = 4 def _GeneralizedPopen(*args, **kwargs): - if os.name == 'nt': + if sys.platform == "win32": try: # Was introduced in Python 3.7. CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW @@ -104,6 +104,8 @@ def _cleanup_process(process, thread): class _InferenceStateProcess: + get_compiled_method_return: Any + def __init__(self, inference_state: 'InferenceState') -> None: self._inference_state_weakref = weakref.ref(inference_state) self._handles: Dict[int, AccessHandle] = {} diff --git a/jedi/inference/compiled/value.py b/jedi/inference/compiled/value.py index b3a841b1..5852adec 100644 --- a/jedi/inference/compiled/value.py +++ b/jedi/inference/compiled/value.py @@ -592,7 +592,7 @@ def create_from_name(inference_state, compiled_value, name): value = create_cached_compiled_value( inference_state, access_path, - parent_context=None if value is None else value.as_context(), + parent_context=None if value is None else value.as_context(), # type: ignore # TODO ) return value @@ -610,7 +610,7 @@ def create_from_access_path(inference_state, access_path): value = create_cached_compiled_value( inference_state, access, - parent_context=None if value is None else value.as_context() + parent_context=None if value is None else value.as_context() # type: ignore # TODO ) return value diff --git a/jedi/inference/context.py b/jedi/inference/context.py index 6645cb4a..080fc8fd 100644 --- a/jedi/inference/context.py +++ b/jedi/inference/context.py @@ -1,7 +1,7 @@ from abc import abstractmethod from contextlib import contextmanager from pathlib import Path -from typing import Optional +from typing import Optional, Any from parso.python.tree import Name @@ -16,6 +16,8 @@ from jedi import parser_utils class AbstractContext: # Must be defined: inference_state and tree_node and parent_context as an attribute/property + tree_node: Any + parent_context: Any def __init__(self, inference_state): self.inference_state = inference_state @@ -218,6 +220,13 @@ class ValueContext(AbstractContext): class TreeContextMixin: + tree_node: Any + is_module: Any + get_value: Any + inference_state: Any + is_class: Any + parent_context: Any + def infer_node(self, node): from jedi.inference.syntax_tree import infer_node return infer_node(self, node) @@ -300,7 +309,6 @@ class TreeContextMixin: class FunctionContext(TreeContextMixin, ValueContext): def get_filters(self, until_position=None, origin_scope=None): yield ParserTreeFilter( - self.inference_state, parent_context=self, until_position=until_position, origin_scope=origin_scope diff --git a/jedi/inference/docstring_utils.py b/jedi/inference/docstring_utils.py index bee0d75e..faa66414 100644 --- a/jedi/inference/docstring_utils.py +++ b/jedi/inference/docstring_utils.py @@ -16,6 +16,6 @@ class DocstringModuleContext(ModuleContext): super().__init__(module_value) self._in_module_context = in_module_context - def get_filters(self, origin_scope=None, until_position=None): + def get_filters(self, until_position=None, origin_scope=None): yield from super().get_filters(until_position=until_position) yield from self._in_module_context.get_filters() diff --git a/jedi/inference/filters.py b/jedi/inference/filters.py index c9fd068a..8d0279b4 100644 --- a/jedi/inference/filters.py +++ b/jedi/inference/filters.py @@ -3,7 +3,7 @@ Filters are objects that you can use to filter names in different scopes. They are needed for name resolution. """ from abc import abstractmethod -from typing import List, MutableMapping, Type +from typing import MutableMapping, Type, Any import weakref from parso.python.tree import Name, UsedNamesMapping @@ -16,8 +16,8 @@ from jedi.inference.utils import to_list from jedi.inference.names import TreeNameDefinition, ParamName, \ AnonymousParamName, AbstractNameDefinition, NameWrapper -_definition_name_cache: MutableMapping[UsedNamesMapping, List[Name]] -_definition_name_cache = weakref.WeakKeyDictionary() +_definition_name_cache: MutableMapping[UsedNamesMapping, dict[str, tuple[Name, ...]]] \ + = weakref.WeakKeyDictionary() class AbstractFilter: @@ -346,6 +346,9 @@ class _OverwriteMeta(type): class _AttributeOverwriteMixin: + overwritten_methods: Any + _wrapped_value: Any + def get_filters(self, *args, **kwargs): yield SpecialMethodFilter(self, self.overwritten_methods, self._wrapped_value) yield from self._wrapped_value.get_filters(*args, **kwargs) diff --git a/jedi/inference/gradual/generics.py b/jedi/inference/gradual/generics.py index f4a5ae9c..4a1cd8a9 100644 --- a/jedi/inference/gradual/generics.py +++ b/jedi/inference/gradual/generics.py @@ -2,6 +2,7 @@ This module is about generics, like the `int` in `List[int]`. It's not about the Generic class. """ +from abc import abstractmethod from jedi import debug from jedi.cache import memoize_method @@ -24,6 +25,14 @@ def _resolve_forward_references(context, value_set): class _AbstractGenericManager: + @abstractmethod + def __getitem__(self, index): + raise NotImplementedError + + @abstractmethod + def to_tuple(self): + raise NotImplementedError + def get_index_and_execute(self, index): try: return self[index].execute_annotation() diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 4466cbf4..f33c908f 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -6,6 +6,7 @@ values. This file deals with all the typing.py cases. """ import itertools +from typing import Any from jedi import debug from jedi.inference.compiled import builtin_from_name, create_simple_object @@ -186,6 +187,8 @@ class ProxyTypingValue(BaseTypingValue): class _TypingClassMixin(ClassMixin): + _tree_name: Any + def py__bases__(self): return [LazyKnownValues( self.inference_state.builtins_module.py__getattribute__('object') diff --git a/jedi/inference/helpers.py b/jedi/inference/helpers.py index 0e344c24..b7dbf4c2 100644 --- a/jedi/inference/helpers.py +++ b/jedi/inference/helpers.py @@ -5,7 +5,7 @@ import os from itertools import chain from contextlib import contextmanager -from parso.python import tree +from parso import tree def is_stdlib_path(path): diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index 1a3acb18..6c0b44f7 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -474,7 +474,7 @@ def _load_python_module(inference_state, file_io, ) -def _load_builtin_module(inference_state, import_names=None, sys_path=None): +def _load_builtin_module(inference_state, import_names, sys_path): project = inference_state.project if sys_path is None: sys_path = inference_state.get_sys_path() diff --git a/jedi/inference/names.py b/jedi/inference/names.py index e7e41ed0..59262428 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -1,9 +1,8 @@ from abc import abstractmethod from inspect import Parameter -from typing import Optional, Tuple +from typing import Optional, Tuple, Any from jedi.parser_utils import find_statement_documentation, clean_scope_docstring -from jedi.inference.utils import unite from jedi.inference.base_value import ValueSet, NO_VALUES from jedi.inference.cache import inference_state_method_cache from jedi.inference import docstrings @@ -37,7 +36,6 @@ class AbstractNameDefinition: def infer(self): raise NotImplementedError - @abstractmethod def goto(self): # Typically names are already definitions and therefore a goto on that # name will always result on itself. @@ -105,6 +103,9 @@ class AbstractArbitraryName(AbstractNameDefinition): class AbstractTreeName(AbstractNameDefinition): + tree_name: Any + parent_context: Any + def __init__(self, parent_context, tree_name): self.parent_context = parent_context self.tree_name = tree_name @@ -194,10 +195,11 @@ class AbstractTreeName(AbstractNameDefinition): new_dotted = deep_ast_copy(par) new_dotted.children[index - 1:] = [] values = context.infer_node(new_dotted) - return unite( - value.goto(name, name_context=context) + return [ + n + for n in value.goto(name, name_context=context) for value in values - ) + ] if node_type == 'trailer' and par.children[0] == '.': values = infer_call_of_leaf(context, name, cut_own_trailer=True) @@ -222,6 +224,9 @@ class AbstractTreeName(AbstractNameDefinition): class ValueNameMixin: + _value: Any + parent_context: Any + def infer(self): return ValueSet([self._value]) @@ -357,6 +362,8 @@ class TreeNameDefinition(AbstractTreeName): class _ParamMixin: + get_kind: Any + def maybe_positional_argument(self, include_star=True): options = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD] if include_star: @@ -629,6 +636,10 @@ class NameWrapper: class StubNameMixin: + api_type: str + tree_name: Any + infer: Any + def py__doc__(self): from jedi.inference.gradual.conversion import convert_names # Stubs are not complicated and we can just follow simple statements @@ -640,7 +651,7 @@ class StubNameMixin: names = convert_names(names, prefer_stub_to_compiled=False) if self in names: - return super().py__doc__() + return super().py__doc__() # type: ignore else: # We have signatures ourselves in stubs, so don't use signatures # from the implementation. diff --git a/jedi/inference/signature.py b/jedi/inference/signature.py index 565a269b..c968bf27 100644 --- a/jedi/inference/signature.py +++ b/jedi/inference/signature.py @@ -1,4 +1,5 @@ from inspect import Parameter +from typing import Any from jedi.cache import memoize_method from jedi import debug @@ -6,6 +7,10 @@ from jedi import parser_utils class _SignatureMixin: + get_param_names: Any + name: Any + annotation_string: Any + def to_string(self): def param_strings(): is_positional = False @@ -36,6 +41,8 @@ class _SignatureMixin: class AbstractSignature(_SignatureMixin): + _function_value: Any + def __init__(self, value, is_bound=False): self.value = value self.is_bound = is_bound diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index e477c683..e4d1b785 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -89,6 +89,7 @@ def infer_node(context, element): if isinstance(context, CompForContext): return _infer_node(context, element) + name_dicts = [{}] if_stmt = element while if_stmt is not None: if_stmt = if_stmt.parent @@ -104,7 +105,6 @@ def infer_node(context, element): if predefined_if_name_dict is None and if_stmt \ and if_stmt.type == 'if_stmt' and context.inference_state.is_analysis: if_stmt_test = if_stmt.children[1] - name_dicts = [{}] # If we already did a check, we don't want to do it again -> If # value.predefined_names is filled, we stop. # We don't want to check the if stmt itself, it's just about @@ -435,7 +435,7 @@ def _infer_expr_stmt(context, stmt, seek_name=None): value_set = ValueSet(to_mod(v) for v in left_values) else: operator = copy.copy(first_operator) - operator.value = operator.value[:-1] + operator.value = operator.value[:-1] # type: ignore[attor-defined] for_stmt = stmt.search_ancestor('for_stmt') if for_stmt is not None and for_stmt.type == 'for_stmt' and value_set \ and parser_utils.for_stmt_defines_one_name(for_stmt): diff --git a/jedi/inference/utils.py b/jedi/inference/utils.py index ab10bcd9..2f8d41f1 100644 --- a/jedi/inference/utils.py +++ b/jedi/inference/utils.py @@ -74,7 +74,6 @@ class PushBackIterator: def __init__(self, iterator): self.pushes = [] self.iterator = iterator - self.current = None def push_back(self, value): self.pushes.append(value) diff --git a/jedi/inference/value/function.py b/jedi/inference/value/function.py index 96351ac9..479503d1 100644 --- a/jedi/inference/value/function.py +++ b/jedi/inference/value/function.py @@ -1,3 +1,5 @@ +from typing import Any + from jedi import debug from jedi.inference.cache import inference_state_method_cache, CachedMetaClass from jedi.inference import compiled @@ -53,6 +55,10 @@ class FunctionAndClassBase(TreeValue): class FunctionMixin: api_type = 'function' + tree_node: Any + py__class__: Any + as_context: Any + get_signature_functions: Any def get_filters(self, origin_scope=None): cls = self.py__class__() diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index cba9eb1e..70babc2b 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -1,4 +1,5 @@ from abc import abstractproperty +from typing import Any from jedi import debug from jedi import settings @@ -187,6 +188,8 @@ class CompiledInstance(AbstractInstanceValue): class _BaseTreeInstance(AbstractInstanceValue): + get_defined_names: Any + @property def array_type(self): name = self.class_value.py__name__() diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index 7cc37173..a4e9f7bb 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -2,6 +2,8 @@ Contains all classes and functions to deal with lists, dicts, generators and iterators in general. """ +from typing import Any + from jedi.inference import compiled from jedi.inference import analysis from jedi.inference.lazy_value import LazyKnownValue, LazyKnownValues, \ @@ -20,6 +22,9 @@ from jedi.inference.value.dynamic_arrays import check_array_additions class IterableMixin: + py__iter__: Any + inference_state: Any + def py__next__(self, contextualized_node=None): return self.py__iter__(contextualized_node) @@ -128,6 +133,12 @@ def comprehension_from_atom(inference_state, value, atom): class ComprehensionMixin: + _defining_context: Any + _entry_node: Any + array_type: Any + _value_node: Any + _sync_comp_for_node: Any + @inference_state_method_cache() def _get_comp_for_context(self, parent_context, comp_for): return CompForContext(parent_context, comp_for) @@ -176,6 +187,8 @@ class ComprehensionMixin: class _DictMixin: + get_mapping_item_values: Any + def _get_generics(self): return tuple(c_set.py__class__() for c_set in self.get_mapping_item_values()) @@ -248,6 +261,9 @@ class GeneratorComprehension(_BaseComprehension, GeneratorBase): class _DictKeyMixin: + _dict_keys: Any + _dict_values: Any + # TODO merge with _DictMixin? def get_mapping_item_values(self): return self._dict_keys(), self._dict_values() diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 5377cf7b..01844249 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -38,7 +38,7 @@ py__doc__() Returns the docstring for a value. """ from __future__ import annotations -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, TYPE_CHECKING, Any from jedi import debug from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted, \ @@ -61,6 +61,9 @@ from inspect import Parameter from jedi.inference.names import BaseTreeParamName from jedi.inference.signature import AbstractSignature +if TYPE_CHECKING: + from jedi.inference import InferenceState + class ClassName(TreeNameDefinition): def __init__(self, class_value, tree_name, name_context, apply_decorators): @@ -197,6 +200,15 @@ def get_dataclass_param_names(cls) -> List[DataclassParamName]: class ClassMixin: + tree_node: Any + parent_context: Any + inference_state: InferenceState + py__bases__: Any + get_metaclasses: Any + get_metaclass_filters: Any + get_metaclass_signatures: Any + list_type_vars: Any + def is_class(self): return True diff --git a/jedi/inference/value/module.py b/jedi/inference/value/module.py index bfcd980e..374880b7 100644 --- a/jedi/inference/value/module.py +++ b/jedi/inference/value/module.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Optional +from typing import Optional, TYPE_CHECKING, Any from jedi.inference.cache import inference_state_method_cache from jedi.inference.names import AbstractNameDefinition, ModuleName @@ -13,6 +13,9 @@ from jedi.inference.compiled import create_simple_object from jedi.inference.base_value import ValueSet from jedi.inference.context import ModuleContext +if TYPE_CHECKING: + from jedi.inference import InferenceState + class _ModuleAttributeName(AbstractNameDefinition): """ @@ -35,6 +38,11 @@ class _ModuleAttributeName(AbstractNameDefinition): class SubModuleDictMixin: + inference_state: "InferenceState" + is_package: Any + py__path__: Any + as_context: Any + @inference_state_method_cache() def sub_modules_dict(self): """ @@ -57,6 +65,10 @@ class SubModuleDictMixin: class ModuleMixin(SubModuleDictMixin): _module_name_class = ModuleName + tree_node: Any + string_names: Any + sub_modules_dict: Any + py__file__: Any def get_filters(self, origin_scope=None): yield MergedFilter( diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index ddec28e8..5dc5f93c 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -259,7 +259,7 @@ def get_parent_scope(node, include_flows=False): # the if, but the parent of the if. if not (scope.type == 'if_stmt' and any(n.start_pos <= node.start_pos < n.end_pos - for n in scope.get_test_nodes())): + for n in scope.get_test_nodes())): # type: ignore[attr-defined] return scope scope = scope.parent diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 1794a98e..93e48058 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -192,7 +192,7 @@ def _iter_pytest_modules(module_context, skip_own_module=False): folder = folder.get_parent_folder() # prevent an infinite for loop if the same parent folder is return twice - if last_folder is not None and folder.path == last_folder.path: + if last_folder is not None and folder.path == last_folder.path: # type: ignore # TODO break last_folder = folder # keep track of the last found parent name diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index ab641270..fba94894 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -134,7 +134,7 @@ def execute(callback): except KeyError: pass else: - return func(value, arguments=arguments, callback=call) + return func(value, arguments=arguments, callback=call) # type: ignore return call() return wrapper diff --git a/pyproject.toml b/pyproject.toml index 97364983..dd4c7453 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -[tool.mypy] +[tool.zuban] # Exclude our copies of external stubs exclude = "^jedi/third_party" @@ -8,25 +8,11 @@ enable_error_code = "ignore-without-code" # Ensure generics are explicit about what they are (e.g: `List[str]` rather than # just `List`) disallow_any_generics = true - disallow_subclassing_any = true # Avoid creating future gotchas emerging from bad typing warn_redundant_casts = true -warn_unused_ignores = true warn_return_any = true warn_unused_configs = true -warn_unreachable = true - -# Require values to be explicitly re-exported; this makes things easier for -# Flake8 too and avoids accidentally importing thing from the "wrong" place -# (which helps avoid circular imports) -implicit_reexport = false - strict_equality = true - -[[tool.mypy.overrides]] -# Various __init__.py files which contain re-exports we want to implicitly make. -module = ["jedi", "jedi.inference.compiled", "jedi.inference.value", "parso"] -implicit_reexport = true From 3ffed76884e3697299e23bb99d22400ba304ae2d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 4 Feb 2026 01:28:30 +0100 Subject: [PATCH 02/18] Improve a weird typing issue --- jedi/inference/imports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index 6c0b44f7..a847015b 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -370,16 +370,16 @@ def import_module_by_names(inference_state, import_names, sys_path=None, i.value if isinstance(i, tree.Name) else i for i in import_names ) - value_set = [None] + base = [None] for i, name in enumerate(import_names): - value_set = ValueSet.from_sets([ + base = value_set = ValueSet.from_sets([ import_module( inference_state, str_import_names[:i+1], parent_module_value, sys_path, - prefer_stubs=prefer_stubs, - ) for parent_module_value in value_set + prefer_stubs=prefer_stubs, # type: ignore[call-arg] + ) for parent_module_value in base ]) if not value_set: message = 'No module named ' + '.'.join(str_import_names) From e7fdbcc834e57e75733d7e3776467511794b5c79 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 4 Feb 2026 01:55:53 +0100 Subject: [PATCH 03/18] Fix a few more typing issues --- jedi/api/completion.py | 3 ++- jedi/api/environment.py | 2 +- jedi/cache.py | 2 +- jedi/debug.py | 2 +- jedi/inference/__init__.py | 1 + jedi/inference/arguments.py | 2 +- jedi/inference/base_value.py | 15 ++++++++++----- jedi/inference/compiled/getattr_static.py | 4 ++-- jedi/inference/compiled/subprocess/functions.py | 5 ++++- jedi/inference/dynamic_params.py | 4 ++-- jedi/inference/gradual/base.py | 4 ++-- jedi/inference/names.py | 6 +++--- jedi/inference/syntax_tree.py | 2 +- jedi/inference/value/instance.py | 1 + jedi/inference/value/iterable.py | 2 +- jedi/inference/value/klass.py | 3 +++ jedi/plugins/django.py | 3 ++- 17 files changed, 38 insertions(+), 23 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index a9932150..376814a1 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -1,5 +1,6 @@ import re from textwrap import dedent +from typing import Any from inspect import Parameter from parso.python.token import PythonTokenTypes @@ -265,7 +266,7 @@ class Completion: elif type_ == 'for_stmt': allowed_transitions.append('else') - completion_names = [] + completion_names: list[Any] = [] kwargs_only = False if any(t in allowed_transitions for t in (PythonTokenTypes.NAME, diff --git a/jedi/api/environment.py b/jedi/api/environment.py index 0bf9169e..f9f78b08 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -254,7 +254,7 @@ def get_cached_default_environment(): # /path/to/env so we need to fully resolve the paths in order to # compare them. if var and os.path.realpath(var) != os.path.realpath(environment.path): - _get_cached_default_environment.clear_cache() + _get_cached_default_environment.clear_cache() # type: ignore[attr-defined] return _get_cached_default_environment() return environment diff --git a/jedi/cache.py b/jedi/cache.py index 2bb15105..e6e371de 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -93,7 +93,7 @@ def time_cache(seconds): cache[key] = time.time(), result return result - wrapper.clear_cache = lambda: cache.clear() + wrapper.clear_cache = lambda: cache.clear() # type: ignore[attr-defined] return wrapper return decorator diff --git a/jedi/debug.py b/jedi/debug.py index 167be41b..8f2340af 100644 --- a/jedi/debug.py +++ b/jedi/debug.py @@ -36,7 +36,7 @@ try: # pytest resets the stream at the end - causes troubles. Since # after every output the stream is reset automatically we don't # need this. - initialise.atexit_done = True + initialise.atexit_done = True # type: ignore[attr-defined] try: init(strip=False) except Exception: diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index 2d0a7099..b468730e 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -85,6 +85,7 @@ from jedi.plugins import plugin_manager class InferenceState: analysis_modules: list[Any] + def __init__(self, project, environment=None, script_path=None): if environment is None: environment = project.get_environment() diff --git a/jedi/inference/arguments.py b/jedi/inference/arguments.py index b2c28ad3..122a8a67 100644 --- a/jedi/inference/arguments.py +++ b/jedi/inference/arguments.py @@ -135,7 +135,7 @@ class _AbstractArgumentsMixin: class AbstractArguments(_AbstractArgumentsMixin): context = None - argument_node = None + argument_node: Any = None trailer = None diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 6df71e9f..25c4181d 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -351,11 +351,16 @@ class _ValueWrapperBase(HelperValueMixin): class LazyValueWrapper(_ValueWrapperBase): - @safe_property - @memoize_method - def _wrapped_value(self): - with debug.increase_indent_cm('Resolve lazy value wrapper'): - return self._get_wrapped_value() + if TYPE_CHECKING: + @property + def _wrapped_value(self) -> Any: + return + else: + @safe_property + @memoize_method + def _wrapped_value(self): + with debug.increase_indent_cm('Resolve lazy value wrapper'): + return self._get_wrapped_value() def __repr__(self): return '<%s>' % (self.__class__.__name__) diff --git a/jedi/inference/compiled/getattr_static.py b/jedi/inference/compiled/getattr_static.py index 03c199ef..dd02f4d6 100644 --- a/jedi/inference/compiled/getattr_static.py +++ b/jedi/inference/compiled/getattr_static.py @@ -39,7 +39,7 @@ def _is_type(obj): def _shadowed_dict(klass): - dict_attr = type.__dict__["__dict__"] + dict_attr = type.__dict__["__dict__"] # type: ignore[index] for entry in _static_getmro(klass): try: class_dict = dict_attr.__get__(entry)["__dict__"] @@ -54,7 +54,7 @@ def _shadowed_dict(klass): def _static_getmro(klass): - mro = type.__dict__['__mro__'].__get__(klass) + mro = type.__dict__['__mro__'].__get__(klass) # type: ignore[index] if not isinstance(mro, (tuple, list)): # There are unfortunately no tests for this, I was not able to # reproduce this in pure Python. However should still solve the issue diff --git a/jedi/inference/compiled/subprocess/functions.py b/jedi/inference/compiled/subprocess/functions.py index 50c47b83..497c8098 100644 --- a/jedi/inference/compiled/subprocess/functions.py +++ b/jedi/inference/compiled/subprocess/functions.py @@ -158,7 +158,10 @@ def _find_module(string, path=None, full_name=None, is_global_search=True): if loader is None and not spec.has_location: # This is a namespace package. full_name = string if not path else full_name - implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path) + implicit_ns_info = ImplicitNSInfo( + full_name, + spec.submodule_search_locations._path, # type: ignore[union-attr] + ) return implicit_ns_info, True break diff --git a/jedi/inference/dynamic_params.py b/jedi/inference/dynamic_params.py index e759111a..dc15296e 100644 --- a/jedi/inference/dynamic_params.py +++ b/jedi/inference/dynamic_params.py @@ -109,7 +109,7 @@ def _search_function_arguments(module_context, funcdef, string_name): if string_name == '__init__': cls = get_parent_scope(funcdef) if cls.type == 'classdef': - string_name = cls.name.value + string_name = cls.name.value # type: ignore[union-attr] compare_node = cls found_arguments = False @@ -203,7 +203,7 @@ def _check_name_for_execution(inference_state, context, compare_node, name, trai # Here we're trying to find decorators by checking the first # parameter. It's not very generic though. Should find a better # solution that also applies to nested decorators. - param_names = value.parent_context.get_param_names() + param_names = value.parent_context.get_param_names() # type: ignore[attr-defined] if len(param_names) != 1: continue values = param_names[0].infer() diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index ce574297..33bf6201 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -195,7 +195,7 @@ class GenericClass(DefineGenericBaseClass, ClassMixin): @to_list def py__bases__(self): - for base in self._wrapped_value.py__bases__(): + for base in self._wrapped_value.py__bases__(): # type: ignore[attr-defined] yield _LazyGenericBaseClass(self, base, self._generics_manager) def _create_instance_with_generics(self, generics_manager): @@ -384,7 +384,7 @@ class BaseTypingValue(LazyValueWrapper): return _PseudoTreeNameClass(self.parent_context, self._tree_name) def get_signatures(self): - return self._wrapped_value.get_signatures() + return self._wrapped_value.get_signatures() # type: ignore[attr-defined] def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self._tree_name.value) diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 59262428..dc6c348c 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -245,7 +245,7 @@ class ValueNameMixin: def get_root_context(self): if self.parent_context is None: # A module return self._value.as_context() - return super().get_root_context() + return super().get_root_context() # type: ignore def get_defining_qualified_value(self): context = self.parent_context @@ -365,13 +365,13 @@ class _ParamMixin: get_kind: Any def maybe_positional_argument(self, include_star=True): - options = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD] + options: list[int] = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD] if include_star: options.append(Parameter.VAR_POSITIONAL) return self.get_kind() in options def maybe_keyword_argument(self, include_stars=True): - options = [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD] + options: list[int] = [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD] if include_stars: options.append(Parameter.VAR_KEYWORD) return self.get_kind() in options diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index e4d1b785..9baf3bf4 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -435,7 +435,7 @@ def _infer_expr_stmt(context, stmt, seek_name=None): value_set = ValueSet(to_mod(v) for v in left_values) else: operator = copy.copy(first_operator) - operator.value = operator.value[:-1] # type: ignore[attor-defined] + operator.value = operator.value[:-1] for_stmt = stmt.search_ancestor('for_stmt') if for_stmt is not None and for_stmt.type == 'for_stmt' and value_set \ and parser_utils.for_stmt_defines_one_name(for_stmt): diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 70babc2b..0ea89155 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -189,6 +189,7 @@ class CompiledInstance(AbstractInstanceValue): class _BaseTreeInstance(AbstractInstanceValue): get_defined_names: Any + _arguments: Any @property def array_type(self): diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index a4e9f7bb..aea79863 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -233,7 +233,7 @@ class Sequence(LazyAttributeOverwrite, IterableMixin): class _BaseComprehension(ComprehensionMixin): def __init__(self, inference_state, defining_context, sync_comp_for_node, entry_node): assert sync_comp_for_node.type == 'sync_comp_for' - super().__init__(inference_state) + super().__init__(inference_state) # type: ignore[call-arg] self._defining_context = defining_context self._sync_comp_for_node = sync_comp_for_node self._entry_node = entry_node diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 01844249..11c70fe2 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -694,6 +694,9 @@ class ClassValue(ClassMixin, FunctionAndClassBase, metaclass=CachedMetaClass): """ bases_arguments = self._get_bases_arguments() + if bases_arguments is None: + return None + if bases_arguments.argument_node.type != "arglist": # If it is not inheriting from the base model and having # extra parameters, then init behavior is not changed. diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index cd443bbd..c83620d7 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -2,6 +2,7 @@ Module is used to infer Django model fields. """ from inspect import Parameter +from typing import Any from jedi import debug from jedi.inference.cache import inference_state_function_cache @@ -140,7 +141,7 @@ def _new_dict_filter(cls, is_instance): include_metaclasses=False, include_type_when_class=False) ) - dct = { + dct: dict[str, Any] = { name.string_name: DjangoModelName(cls, name, is_instance) for filter_ in reversed(filters) for name in filter_.values() From 6903bc25d5e782b98202c6698dd6609ece15b343 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 10 Feb 2026 19:02:51 +0100 Subject: [PATCH 04/18] Remove an outdated script --- scripts/diff_parser_profile.py | 50 ---------------------------------- 1 file changed, 50 deletions(-) delete mode 100755 scripts/diff_parser_profile.py diff --git a/scripts/diff_parser_profile.py b/scripts/diff_parser_profile.py deleted file mode 100755 index 93a12029..00000000 --- a/scripts/diff_parser_profile.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -""" -Profile a piece of Python code with ``cProfile`` that uses the diff parser. - -Usage: - profile.py [-d] [-s ] - profile.py -h | --help - -Options: - -h --help Show this screen. - -d --debug Enable Jedi internal debugging. - -s Sort the profile results, e.g. cumtime, name [default: time]. -""" - -import cProfile - -from docopt import docopt -from jedi.parser.python import load_grammar -from jedi.parser.diff import DiffParser -from jedi.parser.python import ParserWithRecovery -from jedi.common import splitlines -import jedi - - -def run(parser, lines): - diff_parser = DiffParser(parser) - diff_parser.update(lines) - # Make sure used_names is loaded - parser.module.used_names - - -def main(args): - if args['--debug']: - jedi.set_debug_function(notices=True) - - with open(args['']) as f: - code = f.read() - grammar = load_grammar() - parser = ParserWithRecovery(grammar, code) - # Make sure used_names is loaded - parser.module.used_names - - code = code + '\na\n' # Add something so the diff parser needs to run. - lines = splitlines(code, keepends=True) - cProfile.runctx('run(parser, lines)', globals(), locals(), sort=args['-s']) - - -if __name__ == '__main__': - args = docopt(__doc__) - main(args) From 8cbb817b1293e71fdc8dd4bfac5970fbf9fbd53c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 10 Feb 2026 19:38:55 +0100 Subject: [PATCH 05/18] Fix a lot of test related typing issues --- pyproject.toml | 2 +- scripts/memory_check.py | 2 +- scripts/profile_output.py | 2 +- scripts/wx_check.py | 4 ++-- setup.py | 2 +- sith.py | 2 -- test/conftest.py | 2 +- test/run.py | 2 ++ test/test_api/test_completion.py | 3 ++- test/test_api/test_environment.py | 2 +- test/test_api/test_full_name.py | 3 +++ test/test_api/test_interpreter.py | 22 +++++++++---------- test/test_api/test_refactoring.py | 2 +- test/test_api/test_search.py | 2 +- test/test_inference/test_docstring.py | 4 ++-- .../test_gradual/test_typeshed.py | 2 +- test/test_inference/test_imports.py | 4 ++-- test/test_inference/test_mixed.py | 8 +++---- test/test_inference/test_signature.py | 3 ++- test/test_utils.py | 14 +++++++----- 20 files changed, 47 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dd4c7453..fb794afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.zuban] # Exclude our copies of external stubs -exclude = "^jedi/third_party" +exclude = "^jedi/third_party|^test/(completion|refactor|static_analysis|examples)/" show_error_codes = true enable_error_code = "ignore-without-code" diff --git a/scripts/memory_check.py b/scripts/memory_check.py index 7bbcad2b..96470d9a 100755 --- a/scripts/memory_check.py +++ b/scripts/memory_check.py @@ -13,7 +13,7 @@ Note: This requires the psutil library, available on PyPI. import time import sys import os -import psutil +import psutil # type: ignore[import-untyped] sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) import jedi diff --git a/scripts/profile_output.py b/scripts/profile_output.py index 53e0046c..49c77ad8 100755 --- a/scripts/profile_output.py +++ b/scripts/profile_output.py @@ -56,7 +56,7 @@ def main(args): run(code, i, infer=infer) if args['--precision']: - pstats.f8 = f8 + pstats.f8 = f8 # type: ignore[attr-defined] # TODO this does not seem to exist?! jedi.set_debug_function(notices=args['--debug']) if args['--omit']: diff --git a/scripts/wx_check.py b/scripts/wx_check.py index 6d49aa77..d82bcaf7 100755 --- a/scripts/wx_check.py +++ b/scripts/wx_check.py @@ -17,11 +17,11 @@ import sys try: import urllib.request as urllib2 except ImportError: - import urllib2 + import urllib2 # type: ignore[import-not-found, no-redef] import gc from os.path import abspath, dirname -import objgraph +import objgraph # type: ignore[import-untyped] sys.path.insert(0, dirname(dirname(abspath(__file__)))) import jedi diff --git a/setup.py b/setup.py index 56237a5a..fa00a856 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup(name='jedi', packages=find_packages(exclude=['test', 'test.*']), python_requires='>=3.8', # Python 3.13 grammars are added to parso in 0.8.4 - install_requires=['parso>=0.8.5,<0.9.0'], + install_requires=['parso>=0.8.6,<0.9.0'], extras_require={ 'testing': [ 'pytest<9.0.0', diff --git a/sith.py b/sith.py index c4bd4b2d..1c9d7870 100755 --- a/sith.py +++ b/sith.py @@ -35,7 +35,6 @@ Usage: Options: -h --help Show this screen. --record= Exceptions are recorded in here [default: record.json]. - -f, --fs-cache By default, file system cache is off for reproducibility. -n, --maxtries= Maximum of random tries [default: 100] -d, --debug Jedi print debugging when an error is raised. -s Shows the path/line numbers of every completion before it starts. @@ -187,7 +186,6 @@ def main(arguments): 'pudb' if arguments['--pudb'] else None record = arguments['--record'] - jedi.settings.use_filesystem_cache = arguments['--fs-cache'] if arguments['--debug']: jedi.set_debug_function() diff --git a/test/conftest.py b/test/conftest.py index 057c4b62..abb36f68 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -14,7 +14,7 @@ from jedi.api.interpreter import MixedModuleContext # For interpreter tests sometimes the path of this directory is in the sys # path, which we definitely don't want. So just remove it globally. try: - sys.path.remove(helpers.test_dir) + sys.path.remove(str(helpers.test_dir)) except ValueError: pass diff --git a/test/run.py b/test/run.py index 0fd01b23..7d0d4821 100755 --- a/test/run.py +++ b/test/run.py @@ -168,6 +168,8 @@ class BaseTestCase(object): class IntegrationTestCase(BaseTestCase): + source: str # Defined as a side effect + def __init__(self, test_type, correct, line_nr, column, start, line, path=None, skip_version_info=None): super().__init__(skip_version_info) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index de46223e..e3f661ca 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -3,6 +3,7 @@ import os from textwrap import dedent from itertools import count from pathlib import Path +from typing import Any import pytest @@ -301,7 +302,7 @@ def test_file_path_should_have_completions(Script): assert Script('r"').complete() # See GH #1503 -_dict_keys_completion_tests = [ +_dict_keys_completion_tests: "list[tuple[str, int | None, list[str | Any]]]" = [ ('ints[', 5, ['1', '50', Ellipsis]), ('ints[]', 5, ['1', '50', Ellipsis]), ('ints[1]', 5, ['1', '50', Ellipsis]), diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index ea0c1730..87da7e2b 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -32,7 +32,7 @@ def test_versions(version): try: env = get_system_environment(version) except InvalidPythonEnvironment: - if int(version.replace('.', '')) == str(sys.version_info[0]) + str(sys.version_info[1]): + if version.replace('.', '') == str(sys.version_info[0]) + str(sys.version_info[1]): # At least the current version has to work raise pytest.skip() diff --git a/test/test_api/test_full_name.py b/test/test_api/test_full_name.py index 44690141..b8507cf6 100644 --- a/test/test_api/test_full_name.py +++ b/test/test_api/test_full_name.py @@ -15,6 +15,7 @@ There are three kinds of test: import textwrap from unittest import TestCase +from typing import Any import pytest @@ -22,6 +23,8 @@ import jedi class MixinTestFullName(object): + assertEqual: Any + operation = None @pytest.fixture(autouse=True) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 1aa027bf..e5aa30de 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -78,7 +78,7 @@ def test_numpy_like_non_zero(): def test_nested_resolve(): class XX: - def x(): + def x(): # type: ignore[misc] pass cls = get_completion('XX', locals()) @@ -92,7 +92,7 @@ def test_side_effect_completion(): Python code, however we want references to Python code as well. Therefore we need some mixed kind of magic for tests. """ - _GlobalNameSpace.SideEffectContainer.foo = 1 + _GlobalNameSpace.SideEffectContainer.foo = 1 # type: ignore[attr-defined] side_effect = get_completion('SideEffectContainer', _GlobalNameSpace.__dict__) # It's a class that contains MixedObject. @@ -166,7 +166,7 @@ def test_getitem_side_effects(): # Possible side effects here, should therefore not call this. if True: raise NotImplementedError() - return index + return index # type: ignore[unreachable] foo = Foo2() _assert_interpreter_complete('foo["asdf"].upper', locals(), ['upper']) @@ -198,7 +198,7 @@ def test__getattr__completions(allow_unsafe_getattr, class_is_findable): raise AttributeError(name) def __dir__(self): - return ['foo', 'fbar'] + object.__dir__(self) + return ['foo', 'fbar'] + object.__dir__(self) # type: ignore[operator] if not class_is_findable: CompleteGetattr.__name__ = "something_somewhere" @@ -388,7 +388,7 @@ def test_dir_magic_method(allow_unsafe_getattr): raise AttributeError(name) def __dir__(self): - return ['foo', 'bar'] + object.__dir__(self) + return ['foo', 'bar'] + object.__dir__(self) # type: ignore[operator] itp = jedi.Interpreter("ca.", [{'ca': CompleteAttrs()}]) completions = itp.complete() @@ -410,7 +410,7 @@ def test_dir_magic_method(allow_unsafe_getattr): def test_name_not_findable(): class X(): if 0: - NOT_FINDABLE # noqa: F821 + NOT_FINDABLE # type: ignore[unreachable] # noqa: F821 def hidden(self): return @@ -493,7 +493,7 @@ def test__wrapped__(): def test_illegal_class_instance(): class X: - __class__ = 1 + __class__ = 1 # type: ignore[assignment] X.__name__ = 'asdf' d, = jedi.Interpreter('foo', [{'foo': X()}]).infer() v, = d._name.infer() @@ -537,7 +537,7 @@ def test_partial_signatures(code, expected, index): def test_type_var(): """This was an issue before, see Github #1369""" - x = typing.TypeVar('myvar') + x = typing.TypeVar('myvar') # type: ignore[misc] def_, = jedi.Interpreter('x', [locals()]).infer() assert def_.name == 'TypeVar' @@ -576,7 +576,7 @@ def test_dict_completion(code, column, expected): strs = {'asdf': 1, """foo""": 2, r'fbar': 3} mixed = {1: 2, 1.10: 4, None: 6, r'a\sdf': 8, b'foo': 9} - class Inherited(dict): + class Inherited(dict): # type: ignore[type-arg] pass inherited = Inherited(blablu=3) @@ -624,10 +624,10 @@ def test_dunders(class_is_findable, code, expected, allow_unsafe_getattr): def __getitem__(self, key) -> int: return 1 - def __iter__(self, key) -> Iterator[str]: + def __iter__(self, key) -> Iterator[str]: # type: ignore[empty-body] pass - def __next__(self, key) -> float: + def __next__(self, key) -> float: # type: ignore[empty-body] pass if not class_is_findable: diff --git a/test/test_api/test_refactoring.py b/test/test_api/test_refactoring.py index f7bc8c91..a0dd95ff 100644 --- a/test/test_api/test_refactoring.py +++ b/test/test_api/test_refactoring.py @@ -54,7 +54,7 @@ def test_rename_mod(Script, dir_with_content): ''').format(dir=dir_with_content) -@pytest.mark.skipif('sys.version_info[:2] < (3, 8)', message="Python 3.8 introduces dirs_exist_ok") +@pytest.mark.skipif('sys.version_info[:2] < (3, 8)', reason="Python 3.8 introduces dirs_exist_ok") def test_namespace_package(Script, tmpdir): origin = get_example_dir('implicit_namespace_package') shutil.copytree(origin, tmpdir.strpath, dirs_exist_ok=True) diff --git a/test/test_api/test_search.py b/test/test_api/test_search.py index a9170679..9672a03a 100644 --- a/test/test_api/test_search.py +++ b/test/test_api/test_search.py @@ -13,7 +13,7 @@ class SomeClass: def twice(self, b): pass - def some_function(): + def some_function(self): pass diff --git a/test/test_inference/test_docstring.py b/test/test_inference/test_docstring.py index 7c4aad5f..6702617c 100644 --- a/test/test_inference/test_docstring.py +++ b/test/test_inference/test_docstring.py @@ -11,14 +11,14 @@ import jedi from ..helpers import test_dir try: - import numpydoc # NOQA + import numpydoc # type: ignore[import-not-found] # NOQA except ImportError: numpydoc_unavailable = True else: numpydoc_unavailable = False try: - import numpy # NOQA + import numpy # type: ignore[import-not-found] # NOQA except ImportError: numpy_unavailable = True else: diff --git a/test/test_inference/test_gradual/test_typeshed.py b/test/test_inference/test_gradual/test_typeshed.py index 6d0cdf74..a7e96b50 100644 --- a/test/test_inference/test_gradual/test_typeshed.py +++ b/test/test_inference/test_gradual/test_typeshed.py @@ -222,7 +222,7 @@ def test_goto_stubs_on_itself(Script, code, type_): def test_module_exists_only_as_stub(Script): try: - import redis # noqa: F401 + import redis # type: ignore[import-untyped] # noqa: F401 except ImportError: pass else: diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index 99e6d960..07543c6b 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -30,13 +30,13 @@ def test_find_module_basic(): def test_find_module_package(): file_io, is_package = _find_module('json') - assert file_io.path.parts[-2:] == ('json', '__init__.py') + assert file_io.path.parts[-2:] == ('json', '__init__.py') # type: ignore[union-attr] assert is_package is True def test_find_module_not_package(): file_io, is_package = _find_module('io') - assert file_io.path.name == 'io.py' + assert file_io.path.name == 'io.py' # type: ignore[union-attr] assert is_package is False diff --git a/test/test_inference/test_mixed.py b/test/test_inference/test_mixed.py index 1bbe64bd..479a4bbf 100644 --- a/test/test_inference/test_mixed.py +++ b/test/test_inference/test_mixed.py @@ -56,10 +56,10 @@ def test_generics_methods(code, expected, class_findable): class Reader(Generic[T]): @classmethod def read(cls) -> T: - return cls() + return cls() # type: ignore[return-value] def method(self) -> T: - return 1 + return 1 # type: ignore[return-value] class Foo(Reader[str]): def transform(self) -> int: @@ -94,7 +94,7 @@ def test_signature(): pass from inspect import Signature, Parameter - some_signature.__signature__ = Signature([ + some_signature.__signature__ = Signature([ # type: ignore[attr-defined] Parameter('bar', kind=Parameter.KEYWORD_ONLY, default=1) ]) @@ -105,7 +105,7 @@ def test_signature(): def test_compiled_signature_annotation_string(): import typing - def func(x: typing.Type, y: typing.Union[typing.Type, int]): + def func(x: typing.Type, y: typing.Union[typing.Type, int]): # type: ignore[type-arg] pass func.__name__ = 'not_func' diff --git a/test/test_inference/test_signature.py b/test/test_inference/test_signature.py index acd6eafb..a09626fd 100644 --- a/test/test_inference/test_signature.py +++ b/test/test_inference/test_signature.py @@ -2,6 +2,7 @@ from textwrap import dedent from operator import eq, ge, lt import re import os +from typing import Any import pytest @@ -448,7 +449,7 @@ def test_dataclass_signature( assert price.name == price_type_infer -dataclass_transform_cases = [ +dataclass_transform_cases: list[Any] = [ # Attributes on the decorated class and its base classes # are not considered to be fields. # 1/ Declare dataclass transformer diff --git a/test/test_utils.py b/test/test_utils.py index 4fc19878..e452f3d8 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,7 +1,9 @@ +from typing import Any + try: import readline except ImportError: - readline = False + readline = False # type: ignore[assignment] import unittest from jedi import utils @@ -15,7 +17,7 @@ class TestSetupReadline(unittest.TestCase): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) - self.namespace = self.NameSpace() + self.namespace: Any = self.NameSpace() utils.setup_readline(self.namespace) def complete(self, text): @@ -47,8 +49,8 @@ class TestSetupReadline(unittest.TestCase): def test_modules(self): import sys import os - self.namespace.sys = sys - self.namespace.os = os + self.namespace.sys = sys # type: ignore[attr-defined] + self.namespace.os = os # type: ignore[attr-defined] try: assert self.complete('os.path.join') == ['os.path.join'] @@ -58,8 +60,8 @@ class TestSetupReadline(unittest.TestCase): c = {'os.' + d for d in dir(os) if d.startswith('ch')} assert set(self.complete('os.ch')) == set(c) finally: - del self.namespace.sys - del self.namespace.os + del self.namespace.sys # type: ignore[attr-defined] + del self.namespace.os # type: ignore[attr-defined] def test_calls(self): s = 'str(bytes' From ea099835662cef3aefc35fae991655e2e38661b0 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 16 Feb 2026 21:20:27 +0100 Subject: [PATCH 06/18] Some small typing improvements for tests --- conftest.py | 2 +- test/test_api/test_interpreter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 146e353d..86ce7b5e 100644 --- a/conftest.py +++ b/conftest.py @@ -136,7 +136,7 @@ def goto_or_help_or_infer(request, Script): def do(code, *args, **kwargs): return getattr(Script(code), request.param)(*args, **kwargs) - do.type = request.param + do.type = request.param # type: ignore[attr-defined] return do diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index e5aa30de..933ad740 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -810,11 +810,11 @@ def test_try_to_use_return_annotation_for_property(class_is_findable): raise BaseException @property - def with_annotation_garbage1(self) -> 'asldjflksjdfljdslkjfsl': # noqa + def with_annotation_garbage1(self) -> 'asldjflksjdfljdslkjfsl': # type: ignore[name-defined] # noqa return Hello() @property - def with_annotation_garbage2(self) -> 'sdf$@@$5*+8': # noqa + def with_annotation_garbage2(self) -> 'sdf & 8': # type: ignore[valid-type] # noqa return Hello() @property From 3176c1dcb87abd529009da0c01e0cad50b08110c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 16 Feb 2026 22:54:32 +0100 Subject: [PATCH 07/18] A bit more solid typing for goto_or_help_or_infer --- conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/conftest.py b/conftest.py index 86ce7b5e..d7c96845 100644 --- a/conftest.py +++ b/conftest.py @@ -133,11 +133,13 @@ def goto_or_help(request, Script): @pytest.fixture(scope='session', params=['goto', 'help', 'infer']) def goto_or_help_or_infer(request, Script): - def do(code, *args, **kwargs): - return getattr(Script(code), request.param)(*args, **kwargs) + class GotoOrHelpOrInfer: + def __call__(self, code, *args, **kwargs): + return getattr(Script(code), request.param)(*args, **kwargs) - do.type = request.param # type: ignore[attr-defined] - return do + type = request.param + + return GotoOrHelpOrInfer() @pytest.fixture(scope='session', params=['goto', 'complete', 'help']) From ffe4ae58773143964e0764bcfb61650d3aaa5a0e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 26 Mar 2026 23:02:08 +0100 Subject: [PATCH 08/18] Replace the mypy check with zuban check --- .github/workflows/ci.yml | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d94d84a..a2bfc232 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Run tests run: | python -m flake8 jedi test setup.py - python -m mypy jedi sith.py setup.py + zuban check coverage: runs-on: ubuntu-24.04 diff --git a/setup.py b/setup.py index fa00a856..ba92a20b 100755 --- a/setup.py +++ b/setup.py @@ -52,8 +52,7 @@ setup(name='jedi', 'qa': [ # latest version on 2025-06-16 'flake8==7.2.0', - # latest version supporting Python 3.6 - 'mypy==1.16', + 'zuban==0.6.2', # Arbitrary pins, latest at the time of pinning 'types-setuptools==80.9.0.20250529', ], From 9f9150694781f5d46cd9653c297cae1e2e70ce7f Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 26 Mar 2026 23:11:19 +0100 Subject: [PATCH 09/18] Enable --strict and then disable some of the errors --- pyproject.toml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fb794afd..831bcc08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,14 @@ [tool.zuban] +strict = true +enable_error_code = ["ignore-without-code"] + +# Revert some --strict specific flags: +allow_untyped_calls = true +allow_untyped_defs = true +allow_incomplete_defs = true +allow_untyped_globals = true +untyped_strict_optional = false +implicit_reexport = true + # Exclude our copies of external stubs exclude = "^jedi/third_party|^test/(completion|refactor|static_analysis|examples)/" - -show_error_codes = true -enable_error_code = "ignore-without-code" - -# Ensure generics are explicit about what they are (e.g: `List[str]` rather than -# just `List`) -disallow_any_generics = true -disallow_subclassing_any = true - -# Avoid creating future gotchas emerging from bad typing -warn_redundant_casts = true -warn_return_any = true -warn_unused_configs = true - -strict_equality = true From 9e582586fa9fe70101733111ecb9fe289414def7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 26 Mar 2026 23:22:39 +0100 Subject: [PATCH 10/18] Merge testing and qa extras_require into dev --- .github/workflows/ci.yml | 6 +++--- setup.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2bfc232..ddcc4b52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: allow-prereleases: true - name: Install dependencies - run: 'pip install .[testing]' + run: 'pip install .[dev]' - name: Run tests run: python -m pytest @@ -43,7 +43,7 @@ jobs: submodules: recursive - name: Install dependencies - run: 'pip install .[qa]' + run: 'pip install .[dev]' - name: Run tests run: | @@ -60,7 +60,7 @@ jobs: submodules: recursive - name: Install dependencies - run: 'pip install .[testing] coverage' + run: 'pip install .[dev] coverage' - name: Run tests run: | diff --git a/setup.py b/setup.py index ba92a20b..c44d1593 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup(name='jedi', # Python 3.13 grammars are added to parso in 0.8.4 install_requires=['parso>=0.8.6,<0.9.0'], extras_require={ - 'testing': [ + 'dev': [ 'pytest<9.0.0', # docopt for sith doctests 'docopt', @@ -48,8 +48,6 @@ setup(name='jedi', 'Django', 'attrs', 'typing_extensions', - ], - 'qa': [ # latest version on 2025-06-16 'flake8==7.2.0', 'zuban==0.6.2', From 68be64b992d2303880680924e9eaaad494b600b8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 26 Mar 2026 23:31:24 +0100 Subject: [PATCH 11/18] Use forward references because 3.8 is still a bit annoying --- jedi/inference/__init__.py | 2 +- jedi/inference/filters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index b468730e..74402ad7 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -84,7 +84,7 @@ from jedi.plugins import plugin_manager class InferenceState: - analysis_modules: list[Any] + analysis_modules: "list[Any]" def __init__(self, project, environment=None, script_path=None): if environment is None: diff --git a/jedi/inference/filters.py b/jedi/inference/filters.py index 8d0279b4..8743a29d 100644 --- a/jedi/inference/filters.py +++ b/jedi/inference/filters.py @@ -16,7 +16,7 @@ from jedi.inference.utils import to_list from jedi.inference.names import TreeNameDefinition, ParamName, \ AnonymousParamName, AbstractNameDefinition, NameWrapper -_definition_name_cache: MutableMapping[UsedNamesMapping, dict[str, tuple[Name, ...]]] \ +_definition_name_cache: 'MutableMapping[UsedNamesMapping, dict[str, tuple[Name, ...]]]' \ = weakref.WeakKeyDictionary() From 56e55e9ed5fcf1df587ffb2eed702258bc497676 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Apr 2026 02:27:28 +0200 Subject: [PATCH 12/18] Upgrade zuban --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c44d1593..2c90b4ff 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ setup(name='jedi', 'typing_extensions', # latest version on 2025-06-16 'flake8==7.2.0', - 'zuban==0.6.2', + 'zuban==0.7.0', # Arbitrary pins, latest at the time of pinning 'types-setuptools==80.9.0.20250529', ], From 30b3acf6d3133f87302476f1381c751e3a634ae5 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Apr 2026 02:44:39 +0200 Subject: [PATCH 13/18] Change the flake8 version to hopefully fix the version issue in CI --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c90b4ff..e189d171 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ setup(name='jedi', 'attrs', 'typing_extensions', # latest version on 2025-06-16 - 'flake8==7.2.0', + 'flake8==7.1.2', 'zuban==0.7.0', # Arbitrary pins, latest at the time of pinning 'types-setuptools==80.9.0.20250529', From edb5462cf56677b1617955b959cb1c0b193d45e5 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Apr 2026 03:05:30 +0200 Subject: [PATCH 14/18] Fix an issue with tests --- jedi/inference/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/inference/names.py b/jedi/inference/names.py index dc6c348c..901d1af5 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -197,8 +197,8 @@ class AbstractTreeName(AbstractNameDefinition): values = context.infer_node(new_dotted) return [ n - for n in value.goto(name, name_context=context) for value in values + for n in value.goto(name, name_context=context) ] if node_type == 'trailer' and par.children[0] == '.': From 1eddf24a509579da8d8e2a71bc47dac6a9ce73cf Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 27 Apr 2026 00:34:17 +0200 Subject: [PATCH 15/18] Remove Python 3.9 and 3.10 --- README.rst | 6 +++--- conftest.py | 18 +----------------- docs/docs/api.rst | 2 +- docs/docs/features.rst | 2 +- docs/docs/testing.rst | 4 ++-- jedi/api/classes.py | 2 +- jedi/api/environment.py | 2 +- jedi/api/replstartup.py | 2 +- jedi/inference/helpers.py | 2 +- setup.py | 4 +--- test/completion/named_expression.py | 1 - test/completion/pep0484_typing.py | 2 -- test/completion/pep0593_annotations.py | 2 -- test/completion/positional_only_params.py | 2 -- test/completion/stdlib.py | 2 -- test/examples/buildout_project/bin/app | 2 +- test/run.py | 2 +- test/test_api/test_classes.py | 2 +- test/test_api/test_environment.py | 2 +- test/test_api/test_refactoring.py | 1 + test/test_inference/test_buildout_detection.py | 2 +- test/test_inference/test_extension.py | 7 +++++-- test/test_inference/test_signature.py | 6 +++--- .../test_parser_utils.py | 2 +- 24 files changed, 28 insertions(+), 51 deletions(-) diff --git a/README.rst b/README.rst index 62f976aa..7b559abe 100644 --- a/README.rst +++ b/README.rst @@ -102,7 +102,7 @@ Features and Limitations Jedi's features are listed here: `Features `_. -You can run Jedi on Python 3.8+ but it should also +You can run Jedi on Python 3.10+ but it should also understand code that is older than those versions. Additionally you should be able to use `Virtualenvs `_ very well. @@ -183,10 +183,10 @@ The test suite uses ``pytest``:: pip install pytest -If you want to test only a specific Python version (e.g. Python 3.8), it is as +If you want to test only a specific Python version (e.g. Python 3.14), it is as easy as:: - python3.8 -m pytest + python3.14 -m pytest For more detailed information visit the `testing documentation `_. diff --git a/conftest.py b/conftest.py index d7c96845..38465076 100644 --- a/conftest.py +++ b/conftest.py @@ -42,7 +42,7 @@ def pytest_addoption(parser): help="Warnings are treated as errors.") parser.addoption("--env", action='store', - help="Execute the tests in that environment (e.g. 39 for python3.9).") + help="Execute the tests in that environment (e.g. 314 for python3.14).") parser.addoption("--interpreter-env", "-I", action='store_true', help="Don't use subprocesses to guarantee having safe " "code execution. Useful for debugging.") @@ -164,19 +164,3 @@ def skip_pre_python311(environment): # This if is just needed to avoid that tests ever skip way more than # they should for all Python versions. pytest.skip() - - -@pytest.fixture() -def skip_pre_python38(environment): - if environment.version_info < (3, 8): - # This if is just needed to avoid that tests ever skip way more than - # they should for all Python versions. - pytest.skip() - - -@pytest.fixture() -def skip_pre_python37(environment): - if environment.version_info < (3, 7): - # This if is just needed to avoid that tests ever skip way more than - # they should for all Python versions. - pytest.skip() diff --git a/docs/docs/api.rst b/docs/docs/api.rst index 8eac9bd6..f85b3b8a 100644 --- a/docs/docs/api.rst +++ b/docs/docs/api.rst @@ -107,7 +107,7 @@ Completions >>> code = '''import json; json.l''' >>> script = jedi.Script(code, path='example.py') >>> script - > + > >>> completions = script.complete(1, 19) >>> completions [, ] diff --git a/docs/docs/features.rst b/docs/docs/features.rst index 72c3bc8a..9540a758 100644 --- a/docs/docs/features.rst +++ b/docs/docs/features.rst @@ -16,7 +16,7 @@ Jedi's main API calls and features are: Basic Features -------------- -- Python 3.8+ support +- Python 3.10+ support - Ignores syntax errors and wrong indentation - Can deal with complex module / function / class structures - Great ``virtualenv``/``venv`` support diff --git a/docs/docs/testing.rst b/docs/docs/testing.rst index 223cc292..458ae9ac 100644 --- a/docs/docs/testing.rst +++ b/docs/docs/testing.rst @@ -7,10 +7,10 @@ The test suite depends on ``pytest``:: pip install pytest -If you want to test only a specific Python version (e.g. Python 3.8), it is as +If you want to test only a specific Python version (e.g. Python 3.14), it is as easy as:: - python3.8 -m pytest + python3.14 -m pytest Tests are also run automatically on `GitHub Actions `_. diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 94988260..1fe89944 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -96,7 +96,7 @@ class BaseName: @property def module_path(self) -> Optional[Path]: """ - Shows the file path of a module. e.g. ``/usr/lib/python3.9/os.py`` + Shows the file path of a module. e.g. ``/usr/lib/python3.14/os.py`` """ module = self._get_module_context() if module.is_stub() or not module.is_compiled(): diff --git a/jedi/api/environment.py b/jedi/api/environment.py index f9f78b08..f03bdc12 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: _VersionInfo = namedtuple('VersionInfo', 'major minor micro') # type: ignore[name-match] -_SUPPORTED_PYTHONS = ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8'] +_SUPPORTED_PYTHONS = ['3.13', '3.12', '3.11', '3.10'] _SAFE_PATHS = ['/usr/bin', '/usr/local/bin'] _CONDA_VAR = 'CONDA_PREFIX' _CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor) diff --git a/jedi/api/replstartup.py b/jedi/api/replstartup.py index e0f23d19..ce57ce0c 100644 --- a/jedi/api/replstartup.py +++ b/jedi/api/replstartup.py @@ -9,7 +9,7 @@ just use IPython instead:: Then you will be able to use Jedi completer in your Python interpreter:: $ python - Python 3.9.2+ (default, Jul 20 2020, 22:15:08) + Python 3.14.0+ (default, Jul 20 2020, 22:15:08) [GCC 4.6.1] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os diff --git a/jedi/inference/helpers.py b/jedi/inference/helpers.py index b7dbf4c2..bcca751f 100644 --- a/jedi/inference/helpers.py +++ b/jedi/inference/helpers.py @@ -10,7 +10,7 @@ from parso import tree def is_stdlib_path(path): # Python standard library paths look like this: - # /usr/lib/python3.9/... + # /usr/lib/python3.14/... # TODO The implementation below is probably incorrect and not complete. parts = path.parts if 'dist-packages' in parts or 'site-packages' in parts: diff --git a/setup.py b/setup.py index e189d171..1857ffed 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup(name='jedi', keywords='python completion refactoring vim', long_description=readme, packages=find_packages(exclude=['test', 'test.*']), - python_requires='>=3.8', + python_requires='>=3.10', # Python 3.13 grammars are added to parso in 0.8.4 install_requires=['parso>=0.8.6,<0.9.0'], extras_require={ @@ -93,8 +93,6 @@ setup(name='jedi', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', diff --git a/test/completion/named_expression.py b/test/completion/named_expression.py index 11293b68..87b88856 100644 --- a/test/completion/named_expression.py +++ b/test/completion/named_expression.py @@ -1,7 +1,6 @@ # For assignment expressions / named expressions / walrus operators / whatever # they are called. -# python >= 3.8 b = (a:=1, a) #? int() diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index 1a0fabea..1eaadbc7 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -476,8 +476,6 @@ dynamic_annotation('') # TypeDict # ------------------------- -# python >= 3.8 - class Foo(typing.TypedDict): foo: str bar: typing.List[float] diff --git a/test/completion/pep0593_annotations.py b/test/completion/pep0593_annotations.py index 8da12928..92013625 100644 --- a/test/completion/pep0593_annotations.py +++ b/test/completion/pep0593_annotations.py @@ -1,5 +1,3 @@ -# python >= 3.9 - from typing import Annotated # This is just a dummy and very meaningless thing to use with to the Annotated diff --git a/test/completion/positional_only_params.py b/test/completion/positional_only_params.py index 3c1108d9..6280498d 100644 --- a/test/completion/positional_only_params.py +++ b/test/completion/positional_only_params.py @@ -1,5 +1,3 @@ -# python >= 3.8 - def positional_only_call(a, /, b): #? str() a diff --git a/test/completion/stdlib.py b/test/completion/stdlib.py index 94eb5328..1686fa2c 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -459,8 +459,6 @@ X().just_partial('')[0] #? str() X().just_partial('')[1] -# python >= 3.8 - @functools.lru_cache def x() -> int: ... @functools.lru_cache() diff --git a/test/examples/buildout_project/bin/app b/test/examples/buildout_project/bin/app index e8df4eb6..001c553d 100644 --- a/test/examples/buildout_project/bin/app +++ b/test/examples/buildout_project/bin/app @@ -2,7 +2,7 @@ import sys sys.path[0:0] = [ - '/usr/lib/python3.8/site-packages', + '/usr/lib/python3.14/site-packages', '/tmp/.buildout/eggs/important_package.egg' ] diff --git a/test/run.py b/test/run.py index 7d0d4821..86ca19ca 100755 --- a/test/run.py +++ b/test/run.py @@ -448,7 +448,7 @@ Options: --pdb Enable pdb debugging on fail. -d, --debug Enable text output debugging (please install ``colorama``). --thirdparty Also run thirdparty tests (in ``completion/thirdparty``). - --env A Python version, like 3.9, 3.8, etc. + --env A Python version, like 3.14, 3.13, etc. """ if __name__ == '__main__': import docopt diff --git a/test/test_api/test_classes.py b/test/test_api/test_classes.py index 0c3dac5a..01b7cf2d 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -523,7 +523,7 @@ def test_added_equals_to_params(Script): def test_builtin_module_with_path(Script): """ - This test simply tests if a module from /usr/lib/python3.8/lib-dynload/ has + This test simply tests if a module from /usr/lib/python3.14/lib-dynload/ has a path or not. It shouldn't have a module_path, because that is just confusing. """ diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 87da7e2b..8deb9500 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -26,7 +26,7 @@ def test_find_system_environments(): @pytest.mark.parametrize( 'version', - ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + ['3.10', '3.11', '3.12', '3.13'] ) def test_versions(version): try: diff --git a/test/test_api/test_refactoring.py b/test/test_api/test_refactoring.py index a0dd95ff..ae7b5b31 100644 --- a/test/test_api/test_refactoring.py +++ b/test/test_api/test_refactoring.py @@ -54,6 +54,7 @@ def test_rename_mod(Script, dir_with_content): ''').format(dir=dir_with_content) +# TODO does this test still make sense? @pytest.mark.skipif('sys.version_info[:2] < (3, 8)', reason="Python 3.8 introduces dirs_exist_ok") def test_namespace_package(Script, tmpdir): origin = get_example_dir('implicit_namespace_package') diff --git a/test/test_inference/test_buildout_detection.py b/test/test_inference/test_buildout_detection.py index 01d56a78..3a988508 100644 --- a/test/test_inference/test_buildout_detection.py +++ b/test/test_inference/test_buildout_detection.py @@ -67,7 +67,7 @@ def test_path_from_sys_path_assignment(Script): import sys sys.path[0:0] = [ - {os.path.abspath('/usr/lib/python3.8/site-packages')!r}, + {os.path.abspath('/usr/lib/python3.14/site-packages')!r}, {os.path.abspath('/home/test/.buildout/eggs/important_package.egg')!r}, ] diff --git a/test/test_inference/test_extension.py b/test/test_inference/test_extension.py index 8c9015b5..d7a0c136 100644 --- a/test/test_inference/test_extension.py +++ b/test/test_inference/test_extension.py @@ -31,7 +31,9 @@ def test_get_signatures_stdlib(Script): assert len(sigs[0].params) == 1 -# Check only on linux 64 bit platform and Python3.8. +# TODO This is currently only checked on linux 64 bit platform and Python3.8, +# which we don't support anymore, this test should be rewritten (or the +# extension recreated). @pytest.mark.parametrize('load_unsafe_extensions', [False, True]) @pytest.mark.skipif( 'sys.platform != "linux" or sys.maxsize <= 2**32 or sys.version_info[:2] != (3, 8)', @@ -48,7 +50,8 @@ def test_init_extension_module(Script, load_unsafe_extensions): `__init__.cpython-38m.so` by compiling it (create a virtualenv and run `setup.py install`. - This is also why this test only runs on certain systems and Python 3.8. + This is also why this test only runs on certain systems and a specific + Python version. """ project = jedi.Project(get_example_dir(), load_unsafe_extensions=load_unsafe_extensions) diff --git a/test/test_inference/test_signature.py b/test/test_inference/test_signature.py index a09626fd..edac7f26 100644 --- a/test/test_inference/test_signature.py +++ b/test/test_inference/test_signature.py @@ -406,7 +406,7 @@ def test_wraps_signature(Script, code, signature): ], ) def test_dataclass_signature( - Script, skip_pre_python37, start, start_params, include_params, environment + Script, start, start_params, include_params, environment ): if environment.version_info < (3, 8): # Final is not yet supported @@ -725,7 +725,7 @@ ids = [ 'start, start_params, include_params', dataclass_transform_cases, ids=ids ) def test_extensions_dataclass_transform_signature( - Script, skip_pre_python37, start, start_params, include_params, environment + Script, start, start_params, include_params, environment ): has_typing_ext = bool(Script('import typing_extensions').infer()) if not has_typing_ext: @@ -846,7 +846,7 @@ def test_dataclass_transform_signature( ], ids=["define", "frozen", "define_customized", "define_subclass", "define_both"] ) -def test_attrs_signature(Script, skip_pre_python37, start, start_params): +def test_attrs_signature(Script, start, start_params): has_attrs = bool(Script('import attrs').infer()) if not has_attrs: raise pytest.skip("attrs needed in target environment to run this test") diff --git a/test/test_parso_integration/test_parser_utils.py b/test/test_parso_integration/test_parser_utils.py index d29bf751..47546a9e 100644 --- a/test/test_parso_integration/test_parser_utils.py +++ b/test/test_parso_integration/test_parser_utils.py @@ -67,7 +67,7 @@ def test_hex_values_in_docstring(): ('lambda x, y, z: x + y * z\n', '(x, y, z)') ]) def test_get_signature(code, signature): - node = parse(code, version='3.8').children[0] + node = parse(code, version='3.14').children[0] if node.type == 'simple_stmt': node = node.children[0] assert parser_utils.get_signature(node) == signature From a662298e2f7d1914d066a7017ca96ae7091b1ecc Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 27 Apr 2026 00:42:07 +0200 Subject: [PATCH 16/18] Remove a bit more python3.8/3.9 specific code --- jedi/plugins/pytest.py | 26 ++++++-------------------- test/test_api/test_refactoring.py | 2 -- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 93e48058..ae27da4e 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -138,28 +138,14 @@ def _find_pytest_plugin_modules() -> List[List[str]]: See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points """ - if sys.version_info >= (3, 8): - from importlib.metadata import entry_points - - if sys.version_info >= (3, 10): - pytest_entry_points = entry_points(group="pytest11") - else: - pytest_entry_points = entry_points().get("pytest11", ()) - - if sys.version_info >= (3, 9): - return [ep.module.split(".") for ep in pytest_entry_points] - else: - # Python 3.8 doesn't have `EntryPoint.module`. Implement equivalent - # to what Python 3.9 does (with additional None check to placate `mypy`) - matches = [ - ep.pattern.match(ep.value) - for ep in pytest_entry_points - ] - return [x.group('module').split(".") for x in matches if x] + from importlib.metadata import entry_points + if sys.version_info >= (3, 10): + pytest_entry_points = entry_points(group="pytest11") else: - from pkg_resources import iter_entry_points - return [ep.module_name.split(".") for ep in iter_entry_points(group="pytest11")] + pytest_entry_points = entry_points().get("pytest11", ()) + + return [ep.module.split(".") for ep in pytest_entry_points] @inference_state_method_cache() diff --git a/test/test_api/test_refactoring.py b/test/test_api/test_refactoring.py index ae7b5b31..afbacb2b 100644 --- a/test/test_api/test_refactoring.py +++ b/test/test_api/test_refactoring.py @@ -54,8 +54,6 @@ def test_rename_mod(Script, dir_with_content): ''').format(dir=dir_with_content) -# TODO does this test still make sense? -@pytest.mark.skipif('sys.version_info[:2] < (3, 8)', reason="Python 3.8 introduces dirs_exist_ok") def test_namespace_package(Script, tmpdir): origin = get_example_dir('implicit_namespace_package') shutil.copytree(origin, tmpdir.strpath, dirs_exist_ok=True) From 59382622275861355a321b6c8247ca426dd62a99 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 27 Apr 2026 00:59:28 +0200 Subject: [PATCH 17/18] Avoid 3.8/3.9 in tests --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddcc4b52..39044aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ jobs: strategy: matrix: os: [ubuntu-24.04, windows-2022] - python-version: ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8"] - environment: ['3.8', '3.13', '3.12', '3.11', '3.10', '3.9', 'interpreter'] + python-version: ["3.13", "3.12", "3.11", "3.10"] + environment: ['3.13', '3.12', '3.11', '3.10', 'interpreter'] steps: - name: Checkout code uses: actions/checkout@v4 From f8c8ef8c66c5a8f5234a407bf2f2357db2ca55f0 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 27 Apr 2026 01:40:54 +0200 Subject: [PATCH 18/18] Upgrade all the sphinx dependencies --- .readthedocs.yml | 4 ++-- docs/docs/usage.rst | 2 +- setup.py | 52 +++++++++++++++++++++++---------------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 58505775..471b15f0 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,8 +14,8 @@ sphinx: configuration: docs/conf.py build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" + python: "3.14" apt_packages: - graphviz diff --git a/docs/docs/usage.rst b/docs/docs/usage.rst index d9b30955..b3540fb7 100644 --- a/docs/docs/usage.rst +++ b/docs/docs/usage.rst @@ -13,7 +13,7 @@ Below you can also find a list of :ref:`recipes for type hinting `. .. _language-servers: Language Servers --------------- +---------------- - `jedi-language-server `_ - `python-language-server `_ (currently unmaintained) diff --git a/setup.py b/setup.py index 1857ffed..4e772d48 100755 --- a/setup.py +++ b/setup.py @@ -56,31 +56,33 @@ setup(name='jedi', ], 'docs': [ # Just pin all of these. - 'Jinja2==2.11.3', - 'MarkupSafe==1.1.1', - 'Pygments==2.8.1', - 'alabaster==0.7.12', - 'babel==2.9.1', - 'chardet==4.0.0', - 'commonmark==0.8.1', - 'docutils==0.17.1', - 'future==0.18.2', - 'idna==2.10', - 'imagesize==1.2.0', - 'mock==1.0.1', - 'packaging==20.9', - 'pyparsing==2.4.7', - 'pytz==2021.1', - 'readthedocs-sphinx-ext==2.1.4', - 'recommonmark==0.5.0', - 'requests==2.25.1', - 'six==1.15.0', - 'snowballstemmer==2.1.0', - 'sphinx==1.8.5', - 'sphinx-rtd-theme==0.4.3', - 'sphinxcontrib-serializinghtml==1.1.4', - 'sphinxcontrib-websupport==1.2.4', - 'urllib3==1.26.4', + 'alabaster==1.0.0', + 'babel==2.18.0', + 'certifi==2026.4.22', + 'charset-normalizer==3.4.7', + 'docutils==0.22.4', + 'idna==3.13', + 'imagesize==2.0.0', + 'iniconfig==2.3.0', + 'Jinja2==3.1.6', + 'MarkupSafe==3.0.3', + 'packaging==26.2', + 'pluggy==1.6.0', + 'Pygments==2.20.0', + 'pytest==9.0.3', + 'requests==2.33.1', + 'roman-numerals==4.1.0', + 'snowballstemmer==3.0.1', + 'Sphinx==9.1.0', + 'sphinx_rtd_theme==3.1.0', + 'sphinxcontrib-applehelp==2.0.0', + 'sphinxcontrib-devhelp==2.0.0', + 'sphinxcontrib-htmlhelp==2.1.0', + 'sphinxcontrib-jquery==4.1', + 'sphinxcontrib-jsmath==1.0.1', + 'sphinxcontrib-qthelp==2.0.0', + 'sphinxcontrib-serializinghtml==2.0.0', + 'urllib3==2.6.3', ], }, package_data={'jedi': ['*.pyi', 'third_party/typeshed/LICENSE',