diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b9dcd5a..b6ed2d7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ #### Goal Zero (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1394: 100% branch coverage (in addition to line coverage) enforced. + - Implemented #1751: Strict typing enforcement (turned on mypy strict mode). ### 5.8.0 March 20th 2021 - Fixed #1631: as import comments can in some cases be duplicated. diff --git a/isort/__init__.py b/isort/__init__.py index fdb1d6e93..e0754da42 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,4 +1,23 @@ """Defines the public isort interface""" +__all__ = ( + "Config", + "ImportKey", + "__version__", + "check_code", + "check_file", + "check_stream", + "code", + "file", + "find_imports_in_code", + "find_imports_in_file", + "find_imports_in_paths", + "find_imports_in_stream", + "place_module", + "place_module_with_reason", + "settings", + "stream", +) + from . import settings from ._version import __version__ from .api import ImportKey diff --git a/isort/_future/__init__.py b/isort/_future/__init__.py index 4d9ef4b76..48028537b 100644 --- a/isort/_future/__init__.py +++ b/isort/_future/__init__.py @@ -1,7 +1,7 @@ import sys if sys.version_info.major <= 3 and sys.version_info.minor <= 6: - from . import _dataclasses as dataclasses # type: ignore + from . import _dataclasses as dataclasses else: import dataclasses # type: ignore diff --git a/isort/api.py b/isort/api.py index 4d6f5c461..fcc0bd231 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,3 +1,19 @@ +__all__ = ( + "ImportKey", + "check_code_string", + "check_file", + "check_stream", + "find_imports_in_code", + "find_imports_in_file", + "find_imports_in_paths", + "find_imports_in_stream", + "place_module", + "place_module_with_reason", + "sort_code_string", + "sort_file", + "sort_stream", +) + import contextlib import shutil import sys @@ -5,7 +21,7 @@ from io import StringIO from itertools import chain from pathlib import Path -from typing import Iterator, Optional, Set, TextIO, Union, cast +from typing import Any, Iterator, Optional, Set, TextIO, Union, cast from warnings import warn from isort import core @@ -57,8 +73,8 @@ def sort_code_string( file_path: Optional[Path] = None, disregard_skip: bool = False, show_diff: Union[bool, TextIO] = False, - **config_kwargs, -): + **config_kwargs: Any, +) -> str: """Sorts any imports within the provided code string, returning a new string with them sorted. - **code**: The string of code with imports that need to be sorted. @@ -93,7 +109,7 @@ def check_code_string( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, - **config_kwargs, + **config_kwargs: Any, ) -> bool: """Checks the order, format, and categorization of imports within the provided code string. Returns `True` if everything is correct, otherwise `False`. @@ -127,7 +143,7 @@ def sort_stream( disregard_skip: bool = False, show_diff: Union[bool, TextIO] = False, raise_on_skip: bool = True, - **config_kwargs, + **config_kwargs: Any, ) -> bool: """Sorts any imports within the provided code stream, outputs to the provided output stream. Returns `True` if anything is modified from the original input stream, otherwise `False`. @@ -215,7 +231,7 @@ def check_stream( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, - **config_kwargs, + **config_kwargs: Any, ) -> bool: """Checks any imports within the provided code stream, returning `False` if any unsorted or incorrectly imports are found or `True` if no problems are identified. @@ -282,7 +298,7 @@ def check_file( file_path: Optional[Path] = None, disregard_skip: bool = True, extension: Optional[str] = None, - **config_kwargs, + **config_kwargs: Any, ) -> bool: """Checks any imports within the provided file, returning `False` if any unsorted or incorrectly imports are found or `True` if no problems are identified. @@ -335,7 +351,7 @@ def sort_file( show_diff: Union[bool, TextIO] = False, write_to_stdout: bool = False, output: Optional[TextIO] = None, - **config_kwargs, + **config_kwargs: Any, ) -> bool: """Sorts and formats any groups of imports imports within the provided file or Path. Returns `True` if the file has been changed, otherwise `False`. @@ -458,7 +474,7 @@ def find_imports_in_code( file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, top_only: bool = False, - **config_kwargs, + **config_kwargs: Any, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided code string. @@ -486,7 +502,7 @@ def find_imports_in_stream( unique: Union[bool, ImportKey] = False, top_only: bool = False, _seen: Optional[Set[str]] = None, - **config_kwargs, + **config_kwargs: Any, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided code stream. @@ -527,7 +543,7 @@ def find_imports_in_file( file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, top_only: bool = False, - **config_kwargs, + **config_kwargs: Any, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source file. @@ -556,7 +572,7 @@ def find_imports_in_paths( file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, top_only: bool = False, - **config_kwargs, + **config_kwargs: Any, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source paths. @@ -581,7 +597,7 @@ def find_imports_in_paths( def _config( - path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs + path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs: Any ) -> Config: if path and ( config is DEFAULT_CONFIG diff --git a/isort/core.py b/isort/core.py index 3bfd9618e..7b2910da5 100644 --- a/isort/core.py +++ b/isort/core.py @@ -445,7 +445,7 @@ def process( return made_changes -def _indented_config(config: Config, indent: str): +def _indented_config(config: Config, indent: str) -> Config: if not indent: return config diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index 5be8a419f..52bd7cf84 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -19,19 +19,19 @@ from isort.utils import exists_case_sensitive try: - from pipreqs import pipreqs + from pipreqs import pipreqs # type: ignore except ImportError: pipreqs = None try: - from pip_api import parse_requirements + from pip_api import parse_requirements # type: ignore except ImportError: parse_requirements = None try: - from requirementslib import Pipfile + from requirementslib import Pipfile # type: ignore except ImportError: Pipfile = None diff --git a/isort/format.py b/isort/format.py index 1db549137..4de09a423 100644 --- a/isort/format.py +++ b/isort/format.py @@ -6,7 +6,7 @@ from typing import Optional, TextIO try: - import colorama + import colorama # type: ignore except ImportError: colorama_unavailable = True else: @@ -48,7 +48,7 @@ def show_unified_diff( file_path: Optional[Path], output: Optional[TextIO] = None, color_output: bool = False, -): +) -> None: """Shows a unified_diff for the provided input and output against the provided file path. - **file_input**: A string that represents the contents of a file before changes. @@ -125,7 +125,7 @@ def __init__(self, error: str, success: str, output: Optional[TextIO]): def style_text(text: str, style: Optional[str] = None) -> str: if style is None: return text - return style + text + colorama.Style.RESET_ALL + return style + text + str(colorama.Style.RESET_ALL) def diff_line(self, line: str) -> None: style = None @@ -138,7 +138,7 @@ def diff_line(self, line: str) -> None: def create_terminal_printer( color: bool, output: Optional[TextIO] = None, error: str = "", success: str = "" -): +) -> BasicPrinter: if color and colorama_unavailable: no_colorama_message = ( "\n" diff --git a/isort/identify.py b/isort/identify.py index 6a4f6d7d8..e45fcb391 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -32,7 +32,7 @@ def statement(self) -> str: import_string += f" as {self.alias}" return import_string - def __str__(self): + def __str__(self) -> str: return ( f"{self.file_path or ''}:{self.line_number} " f"{'indented ' if self.indented else ''}{self.statement()}" diff --git a/isort/io.py b/isort/io.py index 7b107ab02..e7a74bfd7 100644 --- a/isort/io.py +++ b/isort/io.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import Callable, Iterator, TextIO, Union +from typing import Any, Callable, Iterator, TextIO, Union from isort._future import dataclass from isort.exceptions import UnsupportedEncoding @@ -19,7 +19,7 @@ class File: encoding: str @staticmethod - def detect_encoding(filename: str, readline: Callable[[], bytes]): + def detect_encoding(filename: Union[str, Path], readline: Callable[[], bytes]) -> str: try: return tokenize.detect_encoding(readline)[0] except Exception: @@ -33,11 +33,11 @@ def from_contents(contents: str, filename: str) -> "File": ) @property - def extension(self): + def extension(self) -> str: return self.path.suffix.lstrip(".") @staticmethod - def _open(filename): + def _open(filename: Union[str, Path]) -> TextIOWrapper: """Open a file in read only mode using the encoding detected by detect_encoding(). """ @@ -66,7 +66,7 @@ def read(filename: Union[str, Path]) -> Iterator["File"]: class _EmptyIO(StringIO): - def write(self, *args, **kwargs): # skipcq: PTC-W0049 + def write(self, *args: Any, **kwargs: Any) -> None: # type: ignore # skipcq: PTC-W0049 pass diff --git a/isort/literal.py b/isort/literal.py index 0b1838fe3..62feea13f 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -69,10 +69,14 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU return sorted_value_code -def register_type(name: str, kind: type): +def register_type( + name: str, kind: type +) -> Callable[[Callable[[Any, ISortPrettyPrinter], str]], Callable[[Any, ISortPrettyPrinter], str]]: """Registers a new literal sort type.""" - def wrap(function): + def wrap( + function: Callable[[Any, ISortPrettyPrinter], str] + ) -> Callable[[Any, ISortPrettyPrinter], str]: type_mapping[name] = (kind, function) return function @@ -81,7 +85,7 @@ def wrap(function): @register_type("dict", dict) def _dict(value: Dict[Any, Any], printer: ISortPrettyPrinter) -> str: - return printer.pformat(dict(sorted(value.items(), key=lambda item: item[1]))) + return printer.pformat(dict(sorted(value.items(), key=lambda item: item[1]))) # type: ignore @register_type("list", list) diff --git a/isort/main.py b/isort/main.py index f354d82d2..cc344370f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -7,7 +7,7 @@ from gettext import gettext as _ from io import TextIOWrapper from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence, Union from warnings import warn from . import __version__, api, files, sections @@ -15,7 +15,8 @@ from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles -from .settings import VALID_PY_TARGETS, Config, WrapModes +from .settings import VALID_PY_TARGETS, Config +from .wrap_modes import WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 @@ -920,16 +921,16 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: return arguments -def _preconvert(item): +def _preconvert(item: Any) -> Union[str, List[Any]]: """Preconverts objects from native types into JSONifyiable types""" if isinstance(item, (set, frozenset)): return list(item) if isinstance(item, WrapModes): - return item.name + return str(item.name) if isinstance(item, Path): return str(item) if callable(item) and hasattr(item, "__name__"): - return item.__name__ + return str(item.__name__) raise TypeError("Unserializable object {} of type {}".format(item, type(item))) diff --git a/isort/output.py b/isort/output.py index 9c2fe9acf..884bfb636 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Iterable, List, Set, Tuple +from typing import Any, Iterable, List, Optional, Set, Tuple, Type from isort.format import format_simplified @@ -609,19 +609,21 @@ def _normalize_empty_lines(lines: List[str]) -> List[str]: class _LineWithComments(str): comments: List[str] - def __new__(cls, value: str, comments: List[str]): - instance = super().__new__(cls, value) # type: ignore + def __new__( + cls: Type["_LineWithComments"], value: Any, comments: List[str] + ) -> "_LineWithComments": + instance = super().__new__(cls, value) instance.comments = comments return instance -def _ensure_newline_before_comment(output): +def _ensure_newline_before_comment(output: List[str]) -> List[str]: new_output: List[str] = [] - def is_comment(line): - return line and line.startswith("#") + def is_comment(line: Optional[str]) -> bool: + return line.startswith("#") if line else False - for line, prev_line in zip(output, [None] + output): + for line, prev_line in zip(output, [None] + output): # type: ignore if is_comment(line) and prev_line != "" and not is_comment(prev_line): new_output.append("") new_output.append(line) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 4d4ffb922..d8fad9a8d 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -1,9 +1,9 @@ import os import sys from contextlib import contextmanager -from typing import Any, Dict, List +from typing import Any, Dict, Iterator, List -from pylama.lint import Linter as BaseLinter +from pylama.lint import Linter as BaseLinter # type: ignore from isort.exceptions import FileSkipped @@ -11,7 +11,7 @@ @contextmanager -def supress_stdout(): +def supress_stdout() -> Iterator[None]: stdout = sys.stdout with open(os.devnull, "w") as devnull: sys.stdout = devnull @@ -19,7 +19,7 @@ def supress_stdout(): sys.stdout = stdout -class Linter(BaseLinter): +class Linter(BaseLinter): # type: ignore def allow(self, path: str) -> bool: """Determine if this path should be linted.""" return path.endswith(".py") diff --git a/isort/settings.py b/isort/settings.py index 37dd67a07..5b70eb07f 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -14,7 +14,20 @@ import sys from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Iterable, + List, + Optional, + Pattern, + Set, + Tuple, + Type, + Union, +) from warnings import warn from . import stdlibs @@ -220,7 +233,7 @@ class _Config: format_error: str = "{error}: {message}" format_success: str = "{success}: {message}" - def __post_init__(self): + def __post_init__(self) -> None: py_version = self.py_version if py_version == "auto": # pragma: no cover if sys.version_info.major == 2 and sys.version_info.minor <= 6: @@ -261,7 +274,7 @@ def __post_init__(self): f"{self.wrap_length} > {self.line_length}." ) - def __hash__(self): + def __hash__(self) -> int: return id(self) @@ -274,7 +287,7 @@ def __init__( settings_file: str = "", settings_path: str = "", config: Optional[_Config] = None, - **config_overrides, + **config_overrides: Any, ): self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None self._section_comments: Optional[Tuple[str, ...]] = None @@ -487,7 +500,7 @@ def __init__( super().__init__(sources=tuple(sources), **combined_config) # type: ignore - def is_supported_filetype(self, file_name: str): + def is_supported_filetype(self, file_name: str) -> bool: _root, ext = os.path.splitext(file_name) ext = ext.lstrip(".") if ext in self.supported_extensions: @@ -590,7 +603,7 @@ def is_skipped(self, file_path: Path) -> bool: return False @property - def known_patterns(self): + def known_patterns(self) -> List[Tuple[Pattern[str], str]]: if self._known_patterns is not None: return self._known_patterns @@ -651,8 +664,10 @@ def _parse_known_pattern(self, pattern: str) -> List[str]: return patterns -def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: - type_converter: Callable[[str], Any] = type(_DEFAULT_SETTINGS.get(setting_name, "")) +def _get_str_to_type_converter(setting_name: str) -> Union[Callable[[str], Any], Type[Any]]: + type_converter: Union[Callable[[str], Any], Type[Any]] = type( + _DEFAULT_SETTINGS.get(setting_name, "") + ) if type_converter == WrapModes: type_converter = wrap_mode_from_string return type_converter @@ -742,7 +757,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: and config_key.endswith("}") and extension in map( - lambda text: text.strip(), config_key[len("*.{") : -1].split(",") + lambda text: text.strip(), config_key[len("*.{") : -1].split(",") # type: ignore # noqa ) ): settings.update(config.items(config_key)) diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 6906604ed..d60deda61 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -4,13 +4,13 @@ from typing import Any, Dict, Iterator, List from warnings import warn -import setuptools +import setuptools # type: ignore from . import api from .settings import DEFAULT_CONFIG -class ISortCommand(setuptools.Command): +class ISortCommand(setuptools.Command): # type: ignore """The :class:`ISortCommand` class is used by setuptools to perform imports checks on registered modules. """ diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 7118afc24..39143e739 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -5,14 +5,14 @@ import isort.comments -_wrap_modes: Dict[str, Callable[[Any], str]] = {} +_wrap_modes: Dict[str, Callable[..., str]] = {} def from_string(value: str) -> "WrapModes": return getattr(WrapModes, str(value), None) or WrapModes(int(value)) -def formatter_from_string(name: str): +def formatter_from_string(name: str) -> Callable[..., str]: return _wrap_modes.get(name.upper(), grid) @@ -32,18 +32,18 @@ def _wrap_mode_interface( return "" -def _wrap_mode(function): +def _wrap_mode(function: Callable[..., str]) -> Callable[..., str]: """Registers an individual wrap mode. Function name and order are significant and used for creating enum. """ _wrap_modes[function.__name__.upper()] = function - function.__signature__ = signature(_wrap_mode_interface) + function.__signature__ = signature(_wrap_mode_interface) # type: ignore function.__annotations__ = _wrap_mode_interface.__annotations__ return function @_wrap_mode -def grid(**interface): +def grid(**interface: Any) -> str: if not interface["imports"]: return "" @@ -80,11 +80,11 @@ def grid(**interface): interface["comments"] = [] else: interface["statement"] += ", " + next_import - return interface["statement"] + ("," if interface["include_trailing_comma"] else "") + ")" + return f"{interface['statement']}{',' if interface['include_trailing_comma'] else ''})" @_wrap_mode -def vertical(**interface): +def vertical(**interface: Any) -> str: if not interface["imports"]: return "" @@ -113,7 +113,7 @@ def _hanging_indent_end_line(line: str) -> str: @_wrap_mode -def hanging_indent(**interface): +def hanging_indent(**interface: Any) -> str: if not interface["imports"]: return "" @@ -157,7 +157,7 @@ def hanging_indent(**interface): return statement_with_comments return ( _hanging_indent_end_line(interface["statement"]) - + interface["line_separator"] + + str(interface["line_separator"]) + isort.comments.add_to_line( interface["comments"], interface["indent"], @@ -165,11 +165,11 @@ def hanging_indent(**interface): comment_prefix=interface["comment_prefix"].lstrip(), ) ) - return interface["statement"] + return str(interface["statement"]) @_wrap_mode -def vertical_hanging_indent(**interface): +def vertical_hanging_indent(**interface: Any) -> str: _line_with_comments = isort.comments.add_to_line( interface["comments"], "", @@ -184,7 +184,7 @@ def vertical_hanging_indent(**interface): ) -def _vertical_grid_common(need_trailing_char: bool, **interface): +def _vertical_grid_common(need_trailing_char: bool, **interface: Any) -> str: if not interface["imports"]: return "" @@ -217,32 +217,32 @@ def _vertical_grid_common(need_trailing_char: bool, **interface): interface["statement"] = next_statement if interface["include_trailing_comma"]: interface["statement"] += "," - return interface["statement"] + return str(interface["statement"]) @_wrap_mode -def vertical_grid(**interface) -> str: +def vertical_grid(**interface: Any) -> str: return _vertical_grid_common(need_trailing_char=True, **interface) + ")" @_wrap_mode -def vertical_grid_grouped(**interface): +def vertical_grid_grouped(**interface: Any) -> str: return ( _vertical_grid_common(need_trailing_char=False, **interface) - + interface["line_separator"] + + str(interface["line_separator"]) + ")" ) @_wrap_mode -def vertical_grid_grouped_no_comma(**interface): +def vertical_grid_grouped_no_comma(**interface: Any) -> str: # This is a deprecated alias for vertical_grid_grouped above. This function # needs to exist for backwards compatibility but should never get called. raise NotImplementedError @_wrap_mode -def noqa(**interface): +def noqa(**interface: Any) -> str: _imports = ", ".join(interface["imports"]) retval = f"{interface['statement']}{_imports}" comment_str = " ".join(interface["comments"]) @@ -262,7 +262,7 @@ def noqa(**interface): @_wrap_mode -def vertical_hanging_indent_bracket(**interface): +def vertical_hanging_indent_bracket(**interface: Any) -> str: if not interface["imports"]: return "" statement = vertical_hanging_indent(**interface) @@ -270,7 +270,7 @@ def vertical_hanging_indent_bracket(**interface): @_wrap_mode -def vertical_prefix_from_module_import(**interface): +def vertical_prefix_from_module_import(**interface: Any) -> str: if not interface["imports"]: return "" @@ -306,11 +306,11 @@ def vertical_prefix_from_module_import(**interface): if comments and statement_with_comments: output_statement = statement_with_comments - return output_statement + return str(output_statement) @_wrap_mode -def hanging_indent_with_parentheses(**interface): +def hanging_indent_with_parentheses(**interface: Any) -> str: if not interface["imports"]: return "" @@ -366,7 +366,7 @@ def hanging_indent_with_parentheses(**interface): @_wrap_mode -def backslash_grid(**interface): +def backslash_grid(**interface: Any) -> str: interface["indent"] = interface["white_space"][:-1] return hanging_indent(**interface) diff --git a/setup.cfg b/setup.cfg index aa714e7a9..cff281a90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,7 @@ [mypy] python_version = 3.6 +strict = True follow_imports = silent -disallow_any_generics = True -strict_optional = True -check_untyped_defs = True -allow_redefinition = True -ignore_missing_imports = True [mypy-isort.isort._vendored.*] ignore_errors = True