Introduce some stricter typing

This commit is contained in:
Dave Halter
2026-02-04 01:19:14 +01:00
parent c7481b3319
commit 30ef824abd
28 changed files with 171 additions and 67 deletions

View File

@@ -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):

View File

@@ -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 ''

View File

@@ -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))

View File

@@ -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()

View File

@@ -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.

View File

@@ -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)

View File

@@ -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] = {}

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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')

View File

@@ -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):

View File

@@ -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()

View File

@@ -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.

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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__()

View File

@@ -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__()

View File

@@ -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()

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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