diff --git a/.travis.yml b/.travis.yml index 89102f4a..1588ee1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,8 +30,8 @@ matrix: install: - 'pip install .[qa]' script: - # Ignore F401, which are unused imports. flake8 is a primitive tool and is sometimes wrong. - - 'flake8 --extend-ignore F401 jedi setup.py' + - 'flake8 jedi setup.py' + - 'mypy jedi sith.py' install: - sudo apt-get -y install python3-venv - pip install .[testing] diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 05ee3885..a8d9fb19 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -15,18 +15,18 @@ the interesting information about all operations. """ import re import warnings +from pathlib import Path from typing import Optional -from parso.python.tree import search_ancestor +from parso.tree import search_ancestor from jedi import settings from jedi import debug from jedi.inference.utils import unite from jedi.cache import memoize_method -from jedi.inference import imports -from jedi.inference.imports import ImportName from jedi.inference.compiled.mixed import MixedName -from jedi.inference.gradual.typeshed import StubModuleValue +from jedi.inference.names import ImportName, SubModuleName +from jedi.inference.gradual.stub_value import StubModuleValue from jedi.inference.gradual.conversion import convert_names, convert_values from jedi.inference.base_value import ValueSet from jedi.api.keywords import KeywordName @@ -93,17 +93,15 @@ class BaseName: return self._name.get_root_context() @property - def module_path(self) -> Optional[str]: + def module_path(self) -> Optional[Path]: """ Shows the file path of a module. e.g. ``/usr/lib/python3.9/os.py`` - - :rtype: str or None """ module = self._get_module_context() if module.is_stub() or not module.is_compiled(): # Compiled modules should not return a module path even if they # have one. - path = self._get_module_context().py__file__() + path: Optional[Path] = self._get_module_context().py__file__() if path is not None: return path @@ -186,7 +184,7 @@ class BaseName: tree_name.is_definition(): resolve = True - if isinstance(self._name, imports.SubModuleName) or resolve: + if isinstance(self._name, SubModuleName) or resolve: for value in self._name.infer(): return value.api_type return self._name.api_type @@ -497,7 +495,7 @@ class BaseName: return [self if n == self._name else Name(self._inference_state, n) for n in resulting_names] - @property + @property # type: ignore[misc] @memoize_method def params(self): warnings.warn( diff --git a/jedi/api/completion_cache.py b/jedi/api/completion_cache.py index 6dac6a4e..46e9bead 100644 --- a/jedi/api/completion_cache.py +++ b/jedi/api/completion_cache.py @@ -1,7 +1,13 @@ -_cache = {} +from typing import Dict, Tuple, Callable + +CacheValues = Tuple[str, str, str] +CacheValuesCallback = Callable[[], CacheValues] -def save_entry(module_name, name, cache): +_cache: Dict[str, Dict[str, CacheValues]] = {} + + +def save_entry(module_name: str, name: str, cache: CacheValues) -> None: try: module_cache = _cache[module_name] except KeyError: @@ -9,8 +15,8 @@ def save_entry(module_name, name, cache): module_cache[name] = cache -def _create_get_from_cache(number): - def _get_from_cache(module_name, name, get_cache_values): +def _create_get_from_cache(number: int) -> Callable[[str, str, CacheValuesCallback], str]: + def _get_from_cache(module_name: str, name: str, get_cache_values: CacheValuesCallback) -> str: try: return _cache[module_name][name][number] except KeyError: diff --git a/jedi/api/environment.py b/jedi/api/environment.py index f7636140..3f1f238f 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -384,7 +384,8 @@ def _get_executable_path(path, safe=True): def _get_executables_from_windows_registry(version): - import winreg + # https://github.com/python/typeshed/pull/3794 adds winreg + import winreg # type: ignore[import] # TODO: support Python Anaconda. sub_keys = [ diff --git a/jedi/api/keywords.py b/jedi/api/keywords.py index a0146de0..95c49227 100644 --- a/jedi/api/keywords.py +++ b/jedi/api/keywords.py @@ -1,10 +1,13 @@ import pydoc from contextlib import suppress +from typing import Dict, Optional from jedi.inference.names import AbstractArbitraryName try: - from pydoc_data import topics as pydoc_topics + # https://github.com/python/typeshed/pull/4351 adds pydoc_data + from pydoc_data import topics # type: ignore[import] + pydoc_topics: Optional[Dict[str, str]] = topics.topics except ImportError: # Python 3.6.8 embeddable does not have pydoc_data. pydoc_topics = None @@ -44,6 +47,6 @@ def imitate_pydoc(string): return '' try: - return pydoc_topics.topics[label].strip() if pydoc_topics else '' + return pydoc_topics[label].strip() if pydoc_topics else '' except KeyError: return '' diff --git a/jedi/cache.py b/jedi/cache.py index 29ed979f..1ff45201 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -13,14 +13,15 @@ these variables are being cleaned after every API usage. """ import time from functools import wraps +from typing import Any, Dict, Tuple from jedi import settings from parso.cache import parser_cache -_time_caches = {} +_time_caches: Dict[str, Dict[Any, Tuple[float, Any]]] = {} -def clear_time_caches(delete_all=False): +def clear_time_caches(delete_all: bool = False) -> None: """ Jedi caches many things, that should be completed after each completion finishes. diff --git a/jedi/debug.py b/jedi/debug.py index 4baa0d6a..2b2bcfb8 100644 --- a/jedi/debug.py +++ b/jedi/debug.py @@ -1,6 +1,7 @@ import os import time from contextlib import contextmanager +from typing import Callable, Optional _inited = False @@ -20,7 +21,7 @@ try: raise ImportError else: # Use colorama for nicer console output. - from colorama import Fore, init + from colorama import Fore, init # type: ignore[import] from colorama import initialise def _lazy_colorama_init(): # noqa: F811 @@ -45,7 +46,7 @@ try: _inited = True except ImportError: - class Fore: + class Fore: # type: ignore[no-redef] RED = '' GREEN = '' YELLOW = '' @@ -62,7 +63,7 @@ enable_warning = False enable_notice = False # callback, interface: level, str -debug_function = None +debug_function: Optional[Callable[[str, str], None]] = None _debug_indent = 0 _start_time = time.time() diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index 3461f502..8cf6a1c2 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -120,14 +120,15 @@ class InferenceState: debug.dbg('execute result: %s in %s', value_set, value) return value_set - @property + # mypy doesn't suppport decorated propeties (https://github.com/python/mypy/issues/1362) + @property # type: ignore[misc] @inference_state_function_cache() def builtins_module(self): module_name = 'builtins' builtins_module, = self.import_module((module_name,), sys_path=()) return builtins_module - @property + @property # type: ignore[misc] @inference_state_function_cache() def typing_module(self): typing_module, = self.import_module(('typing',)) diff --git a/jedi/inference/compiled/__init__.py b/jedi/inference/compiled/__init__.py index faf5d373..09ac19f9 100644 --- a/jedi/inference/compiled/__init__.py +++ b/jedi/inference/compiled/__init__.py @@ -1,3 +1,6 @@ +# This file also re-exports symbols for wider use. We configure mypy and flake8 +# to be aware that this file does this. + from jedi.inference.compiled.value import CompiledValue, CompiledName, \ CompiledValueFilter, CompiledValueName, create_from_access_path from jedi.inference.base_value import LazyValueWrapper diff --git a/jedi/inference/compiled/subprocess/__init__.py b/jedi/inference/compiled/subprocess/__init__.py index 13366278..0795de30 100644 --- a/jedi/inference/compiled/subprocess/__init__.py +++ b/jedi/inference/compiled/subprocess/__init__.py @@ -29,19 +29,19 @@ _MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py') PICKLE_PROTOCOL = 4 -class _GeneralizedPopen(subprocess.Popen): - def __init__(self, *args, **kwargs): - if os.name == 'nt': - try: - # Was introduced in Python 3.7. - CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW - except AttributeError: - CREATE_NO_WINDOW = 0x08000000 - kwargs['creationflags'] = CREATE_NO_WINDOW - # The child process doesn't need file descriptors except 0, 1, 2. - # This is unix only. - kwargs['close_fds'] = 'posix' in sys.builtin_module_names - super().__init__(*args, **kwargs) +def _GeneralizedPopen(*args, **kwargs): + if os.name == 'nt': + try: + # Was introduced in Python 3.7. + CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW + except AttributeError: + CREATE_NO_WINDOW = 0x08000000 + kwargs['creationflags'] = CREATE_NO_WINDOW + # The child process doesn't need file descriptors except 0, 1, 2. + # This is unix only. + kwargs['close_fds'] = 'posix' in sys.builtin_module_names + + return subprocess.Popen(*args, **kwargs) def _enqueue_output(out, queue_): diff --git a/jedi/inference/compiled/subprocess/__main__.py b/jedi/inference/compiled/subprocess/__main__.py index 370e2361..e1554334 100644 --- a/jedi/inference/compiled/subprocess/__main__.py +++ b/jedi/inference/compiled/subprocess/__main__.py @@ -1,5 +1,6 @@ import os import sys +from importlib.abc import MetaPathFinder from importlib.machinery import PathFinder # Remove the first entry, because it's simply a directory entry that equals @@ -16,7 +17,7 @@ def _get_paths(): return {'jedi': _jedi_path, 'parso': _parso_path} -class _ExactImporter: +class _ExactImporter(MetaPathFinder): def __init__(self, path_dct): self._path_dct = path_dct diff --git a/jedi/inference/docstrings.py b/jedi/inference/docstrings.py index e9c94cdc..ee7a8d89 100644 --- a/jedi/inference/docstrings.py +++ b/jedi/inference/docstrings.py @@ -50,7 +50,7 @@ def _get_numpy_doc_string_cls(): global _numpy_doc_string_cache if isinstance(_numpy_doc_string_cache, (ImportError, SyntaxError)): raise _numpy_doc_string_cache - from numpydoc.docscrape import NumpyDocString + from numpydoc.docscrape import NumpyDocString # type: ignore[import] _numpy_doc_string_cache = NumpyDocString return _numpy_doc_string_cache diff --git a/jedi/inference/filters.py b/jedi/inference/filters.py index a796c445..2451f468 100644 --- a/jedi/inference/filters.py +++ b/jedi/inference/filters.py @@ -3,9 +3,11 @@ 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 import weakref from parso.tree import search_ancestor +from parso.python.tree import Name, UsedNamesMapping from jedi.inference import flow_analysis from jedi.inference.base_value import ValueSet, ValueWrapper, \ @@ -13,8 +15,9 @@ from jedi.inference.base_value import ValueSet, ValueWrapper, \ from jedi.parser_utils import get_cached_parent_scope from jedi.inference.utils import to_list from jedi.inference.names import TreeNameDefinition, ParamName, \ - AnonymousParamName, AbstractNameDefinition + AnonymousParamName, AbstractNameDefinition, NameWrapper +_definition_name_cache: MutableMapping[UsedNamesMapping, List[Name]] _definition_name_cache = weakref.WeakKeyDictionary() @@ -36,7 +39,7 @@ class AbstractFilter: class FilterWrapper: - name_wrapper_class = None + name_wrapper_class: Type[NameWrapper] def __init__(self, wrapped_filter): self._wrapped_filter = wrapped_filter diff --git a/jedi/inference/flow_analysis.py b/jedi/inference/flow_analysis.py index d5369711..89bfe578 100644 --- a/jedi/inference/flow_analysis.py +++ b/jedi/inference/flow_analysis.py @@ -1,12 +1,14 @@ +from typing import Dict, Optional + from jedi.parser_utils import get_flow_branch_keyword, is_scope, get_parent_scope from jedi.inference.recursion import execution_allowed from jedi.inference.helpers import is_big_annoying_library class Status: - lookup_table = {} + lookup_table: Dict[Optional[bool], 'Status'] = {} - def __init__(self, value, name): + def __init__(self, value: Optional[bool], name: str) -> None: self._value = value self._name = name Status.lookup_table[value] = self diff --git a/jedi/inference/gradual/typeshed.py b/jedi/inference/gradual/typeshed.py index e186bfc3..6295294d 100644 --- a/jedi/inference/gradual/typeshed.py +++ b/jedi/inference/gradual/typeshed.py @@ -2,6 +2,7 @@ import os import re from functools import wraps from collections import namedtuple +from typing import Dict, Mapping, Tuple from pathlib import Path from jedi import settings @@ -74,7 +75,7 @@ def _get_typeshed_directories(version_info): yield PathInfo(str(base_path.joinpath(check_version)), is_third_party) -_version_cache = {} +_version_cache: Dict[Tuple[int, int], Mapping[str, PathInfo]] = {} def _cache_stub_file_map(version_info): diff --git a/jedi/inference/gradual/utils.py b/jedi/inference/gradual/utils.py index 83162625..1fc90e7f 100644 --- a/jedi/inference/gradual/utils.py +++ b/jedi/inference/gradual/utils.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from jedi.inference.gradual.typeshed import TYPESHED_PATH, create_stub_module diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 02d75ea4..38be6f22 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -1,5 +1,6 @@ from abc import abstractmethod from inspect import Parameter +from typing import Optional, Tuple from parso.tree import search_ancestor @@ -24,8 +25,8 @@ def _merge_name_docs(names): class AbstractNameDefinition: - start_pos = None - string_name = None + start_pos: Optional[Tuple[int, int]] = None + string_name: str parent_context = None tree_name = None is_value_name = True diff --git a/jedi/inference/references.py b/jedi/inference/references.py index 07a840a1..7c77f6f8 100644 --- a/jedi/inference/references.py +++ b/jedi/inference/references.py @@ -5,7 +5,8 @@ from parso import python_bytes_to_unicode from jedi.debug import dbg from jedi.file_io import KnownContentFileIO -from jedi.inference.imports import SubModuleName, load_module_from_path +from jedi.inference.names import SubModuleName +from jedi.inference.imports import load_module_from_path from jedi.inference.filters import ParserTreeFilter from jedi.inference.gradual.conversion import convert_names diff --git a/jedi/inference/sys_path.py b/jedi/inference/sys_path.py index 0f44be4a..48b9ac89 100644 --- a/jedi/inference/sys_path.py +++ b/jedi/inference/sys_path.py @@ -14,8 +14,8 @@ from jedi import debug _BUILDOUT_PATH_INSERTION_LIMIT = 10 -def _abs_path(module_context, path: str): - path = Path(path) +def _abs_path(module_context, str_path: str): + path = Path(str_path) if path.is_absolute(): return path diff --git a/jedi/inference/value/__init__.py b/jedi/inference/value/__init__.py index 2e17ba26..62164215 100644 --- a/jedi/inference/value/__init__.py +++ b/jedi/inference/value/__init__.py @@ -1,3 +1,6 @@ +# Re-export symbols for wider use. We configure mypy and flake8 to be aware that +# this file does this. + from jedi.inference.value.module import ModuleValue from jedi.inference.value.klass import ClassValue from jedi.inference.value.function import FunctionValue, \ diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 153ab4f4..9514e01d 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -1,6 +1,6 @@ from abc import abstractproperty -from parso.python.tree import search_ancestor +from parso.tree import search_ancestor from jedi import debug from jedi import settings diff --git a/jedi/inference/value/module.py b/jedi/inference/value/module.py index 128f7185..a5073b0e 100644 --- a/jedi/inference/value/module.py +++ b/jedi/inference/value/module.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Optional from jedi.inference.cache import inference_state_method_cache from jedi.inference.names import AbstractNameDefinition, ModuleName @@ -79,7 +80,7 @@ class ModuleMixin(SubModuleDictMixin): def is_stub(self): return False - @property + @property # type: ignore[misc] @inference_state_method_cache() def name(self): return self._module_name_class(self, self.string_names[-1]) @@ -145,7 +146,7 @@ class ModuleValue(ModuleMixin, TreeValue): ) self.file_io = file_io if file_io is None: - self._path = None + self._path: Optional[Path] = None else: self._path = Path(file_io.path) self.string_names = string_names # Optional[Tuple[str, ...]] @@ -165,7 +166,7 @@ class ModuleValue(ModuleMixin, TreeValue): return None return '.'.join(self.string_names) - def py__file__(self) -> Path: + def py__file__(self) -> Optional[Path]: """ In contrast to Python's __file__ can be None. """ diff --git a/jedi/inference/value/namespace.py b/jedi/inference/value/namespace.py index 0ea9ecf4..fe629a07 100644 --- a/jedi/inference/value/namespace.py +++ b/jedi/inference/value/namespace.py @@ -38,7 +38,7 @@ class ImplicitNamespaceValue(Value, SubModuleDictMixin): def get_qualified_names(self): return () - @property + @property # type: ignore[misc] @inference_state_method_cache() def name(self): string_name = self.py__package__()[-1] diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 432385e3..cec23733 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -1,6 +1,6 @@ from pathlib import Path -from parso.python.tree import search_ancestor +from parso.tree import search_ancestor from jedi.inference.cache import inference_state_method_cache from jedi.inference.imports import load_module_from_path from jedi.inference.filters import ParserTreeFilter diff --git a/jedi/utils.py b/jedi/utils.py index dd529be7..b7e1c1a8 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -2,7 +2,7 @@ Utilities for end-users. """ -import __main__ +import __main__ # type: ignore[import] from collections import namedtuple import logging import traceback diff --git a/setup.cfg b/setup.cfg index 6929ce30..904b5ffe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,40 @@ ignore = W503, # Single letter loop variables are often fine E741, +per-file-ignores = + # Ignore apparently unused imports in files where we're (implicitly) + # re-exporting them. + jedi/__init__.py:F401 + jedi/inference/compiled/__init__.py:F401 + jedi/inference/value/__init__.py:F401 exclude = jedi/third_party/* .tox/* [pycodestyle] max-line-length = 100 + + +[mypy] +# 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 + +[mypy-jedi,jedi.inference.compiled,jedi.inference.value] +# Various __init__.py files which contain re-exports we want to implicitly make. +implicit_reexport = True diff --git a/setup.py b/setup.py index 041ae0b2..1f660a4d 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ setup(name='jedi', ], 'qa': [ 'flake8==3.8.3', + 'mypy==0.782', ], }, package_data={'jedi': ['*.pyi', 'third_party/typeshed/LICENSE', diff --git a/sith.py b/sith.py index 3b286894..7736ab8a 100755 --- a/sith.py +++ b/sith.py @@ -44,7 +44,7 @@ Options: --pudb Launch pudb when error is raised. """ -from docopt import docopt +from docopt import docopt # type: ignore[import] import json import os