123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651 |
- __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
- from enum import Enum
- from io import StringIO
- from itertools import chain
- from pathlib import Path
- from typing import Any, Iterator, Optional, Set, TextIO, Union, cast
- from warnings import warn
- from isort import core
- from . import files, identify, io
- from .exceptions import (
- ExistingSyntaxErrors,
- FileSkipComment,
- FileSkipSetting,
- IntroducedSyntaxErrors,
- )
- from .format import ask_whether_to_apply_changes_to_file, create_terminal_printer, show_unified_diff
- from .io import Empty, File
- from .place import module as place_module # noqa: F401
- from .place import module_with_reason as place_module_with_reason # noqa: F401
- from .settings import CYTHON_EXTENSIONS, DEFAULT_CONFIG, Config
- class ImportKey(Enum):
- """Defines how to key an individual import, generally for deduping.
- Import keys are defined from less to more specific:
- from x.y import z as a
- ______| | | |
- | | | |
- PACKAGE | | |
- ________| | |
- | | |
- MODULE | |
- _________________| |
- | |
- ATTRIBUTE |
- ______________________|
- |
- ALIAS
- """
- PACKAGE = 1
- MODULE = 2
- ATTRIBUTE = 3
- ALIAS = 4
- def sort_code_string(
- code: str,
- extension: Optional[str] = None,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = False,
- show_diff: Union[bool, TextIO] = False,
- **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.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - ****config_kwargs**: Any config modifications.
- """
- input_stream = StringIO(code)
- output_stream = StringIO()
- config = _config(path=file_path, config=config, **config_kwargs)
- sort_stream(
- input_stream,
- output_stream,
- extension=extension,
- config=config,
- file_path=file_path,
- disregard_skip=disregard_skip,
- show_diff=show_diff,
- )
- output_stream.seek(0)
- return output_stream.read()
- def check_code_string(
- code: str,
- show_diff: Union[bool, TextIO] = False,
- extension: Optional[str] = None,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = False,
- **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`.
- - **code**: The string of code with imports that need to be sorted.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - ****config_kwargs**: Any config modifications.
- """
- config = _config(path=file_path, config=config, **config_kwargs)
- return check_stream(
- StringIO(code),
- show_diff=show_diff,
- extension=extension,
- config=config,
- file_path=file_path,
- disregard_skip=disregard_skip,
- )
- def sort_stream(
- input_stream: TextIO,
- output_stream: TextIO,
- extension: Optional[str] = None,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = False,
- show_diff: Union[bool, TextIO] = False,
- raise_on_skip: bool = True,
- **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`.
- - **input_stream**: The stream of code with imports that need to be sorted.
- - **output_stream**: The stream where sorted imports should be written to.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - ****config_kwargs**: Any config modifications.
- """
- extension = extension or (file_path and file_path.suffix.lstrip(".")) or "py"
- if show_diff:
- _output_stream = StringIO()
- _input_stream = StringIO(input_stream.read())
- changed = sort_stream(
- input_stream=_input_stream,
- output_stream=_output_stream,
- extension=extension,
- config=config,
- file_path=file_path,
- disregard_skip=disregard_skip,
- raise_on_skip=raise_on_skip,
- **config_kwargs,
- )
- _output_stream.seek(0)
- _input_stream.seek(0)
- show_unified_diff(
- file_input=_input_stream.read(),
- file_output=_output_stream.read(),
- file_path=file_path,
- output=output_stream if show_diff is True else cast(TextIO, show_diff),
- color_output=config.color_output,
- )
- return changed
- config = _config(path=file_path, config=config, **config_kwargs)
- content_source = str(file_path or "Passed in content")
- if not disregard_skip and file_path and config.is_skipped(file_path):
- raise FileSkipSetting(content_source)
- _internal_output = output_stream
- if config.atomic:
- try:
- file_content = input_stream.read()
- compile(file_content, content_source, "exec", 0, 1)
- except SyntaxError:
- if extension not in CYTHON_EXTENSIONS:
- raise ExistingSyntaxErrors(content_source)
- if config.verbose:
- warn(
- f"{content_source} Python AST errors found but ignored due to Cython extension"
- )
- input_stream = StringIO(file_content)
- if not output_stream.readable():
- _internal_output = StringIO()
- try:
- changed = core.process(
- input_stream,
- _internal_output,
- extension=extension,
- config=config,
- raise_on_skip=raise_on_skip,
- )
- except FileSkipComment:
- raise FileSkipComment(content_source)
- if config.atomic:
- _internal_output.seek(0)
- try:
- compile(_internal_output.read(), content_source, "exec", 0, 1)
- _internal_output.seek(0)
- except SyntaxError: # pragma: no cover
- if extension not in CYTHON_EXTENSIONS:
- raise IntroducedSyntaxErrors(content_source)
- if config.verbose:
- warn(
- f"{content_source} Python AST errors found but ignored due to Cython extension"
- )
- if _internal_output != output_stream:
- output_stream.write(_internal_output.read())
- return changed
- def check_stream(
- input_stream: TextIO,
- show_diff: Union[bool, TextIO] = False,
- extension: Optional[str] = None,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = False,
- **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.
- - **input_stream**: The stream of code with imports that need to be sorted.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - ****config_kwargs**: Any config modifications.
- """
- config = _config(path=file_path, config=config, **config_kwargs)
- if show_diff:
- input_stream = StringIO(input_stream.read())
- changed: bool = sort_stream(
- input_stream=input_stream,
- output_stream=Empty,
- extension=extension,
- config=config,
- file_path=file_path,
- disregard_skip=disregard_skip,
- )
- printer = create_terminal_printer(
- color=config.color_output, error=config.format_error, success=config.format_success
- )
- if not changed:
- if config.verbose and not config.only_modified:
- printer.success(f"{file_path or ''} Everything Looks Good!")
- return True
- printer.error(f"{file_path or ''} Imports are incorrectly sorted and/or formatted.")
- if show_diff:
- output_stream = StringIO()
- input_stream.seek(0)
- file_contents = input_stream.read()
- sort_stream(
- input_stream=StringIO(file_contents),
- output_stream=output_stream,
- extension=extension,
- config=config,
- file_path=file_path,
- disregard_skip=disregard_skip,
- )
- output_stream.seek(0)
- show_unified_diff(
- file_input=file_contents,
- file_output=output_stream.read(),
- file_path=file_path,
- output=None if show_diff is True else cast(TextIO, show_diff),
- color_output=config.color_output,
- )
- return False
- def check_file(
- filename: Union[str, Path],
- show_diff: Union[bool, TextIO] = False,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = True,
- extension: Optional[str] = None,
- **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.
- - **filename**: The name or Path of the file to check.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - ****config_kwargs**: Any config modifications.
- """
- file_config: Config = config
- if "config_trie" in config_kwargs:
- config_trie = config_kwargs.pop("config_trie", None)
- if config_trie:
- config_info = config_trie.search(filename)
- if config.verbose:
- print(f"{config_info[0]} used for file {filename}")
- file_config = Config(**config_info[1])
- with io.File.read(filename) as source_file:
- return check_stream(
- source_file.stream,
- show_diff=show_diff,
- extension=extension,
- config=file_config,
- file_path=file_path or source_file.path,
- disregard_skip=disregard_skip,
- **config_kwargs,
- )
- def _tmp_file(source_file: File) -> Path:
- return source_file.path.with_suffix(source_file.path.suffix + ".isorted")
- @contextlib.contextmanager
- def _in_memory_output_stream_context() -> Iterator[TextIO]:
- yield StringIO(newline=None)
- @contextlib.contextmanager
- def _file_output_stream_context(filename: Union[str, Path], source_file: File) -> Iterator[TextIO]:
- tmp_file = _tmp_file(source_file)
- with tmp_file.open("w+", encoding=source_file.encoding, newline="") as output_stream:
- shutil.copymode(filename, tmp_file)
- yield output_stream
- def sort_file(
- filename: Union[str, Path],
- extension: Optional[str] = None,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- disregard_skip: bool = True,
- ask_to_apply: bool = False,
- show_diff: Union[bool, TextIO] = False,
- write_to_stdout: bool = False,
- output: Optional[TextIO] = None,
- **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`.
- - **filename**: The name or Path of the file to format.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file.
- - **ask_to_apply**: If `True`, prompt before applying any changes.
- - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a
- TextIO stream is provided results will be written to it, otherwise no diff will be computed.
- - **write_to_stdout**: If `True`, write to stdout instead of the input file.
- - **output**: If a TextIO is provided, results will be written there rather than replacing
- the original file content.
- - ****config_kwargs**: Any config modifications.
- """
- file_config: Config = config
- if "config_trie" in config_kwargs:
- config_trie = config_kwargs.pop("config_trie", None)
- if config_trie:
- config_info = config_trie.search(filename)
- if config.verbose:
- print(f"{config_info[0]} used for file {filename}")
- file_config = Config(**config_info[1])
- with io.File.read(filename) as source_file:
- actual_file_path = file_path or source_file.path
- config = _config(path=actual_file_path, config=file_config, **config_kwargs)
- changed: bool = False
- try:
- if write_to_stdout:
- changed = sort_stream(
- input_stream=source_file.stream,
- output_stream=sys.stdout,
- config=config,
- file_path=actual_file_path,
- disregard_skip=disregard_skip,
- extension=extension,
- )
- else:
- if output is None:
- try:
- if config.overwrite_in_place:
- output_stream_context = _in_memory_output_stream_context()
- else:
- output_stream_context = _file_output_stream_context(
- filename, source_file
- )
- with output_stream_context as output_stream:
- changed = sort_stream(
- input_stream=source_file.stream,
- output_stream=output_stream,
- config=config,
- file_path=actual_file_path,
- disregard_skip=disregard_skip,
- extension=extension,
- )
- output_stream.seek(0)
- if changed:
- if show_diff or ask_to_apply:
- source_file.stream.seek(0)
- show_unified_diff(
- file_input=source_file.stream.read(),
- file_output=output_stream.read(),
- file_path=actual_file_path,
- output=None
- if show_diff is True
- else cast(TextIO, show_diff),
- color_output=config.color_output,
- )
- if show_diff or (
- ask_to_apply
- and not ask_whether_to_apply_changes_to_file(
- str(source_file.path)
- )
- ):
- return False
- source_file.stream.close()
- if config.overwrite_in_place:
- output_stream.seek(0)
- with source_file.path.open("w") as fs:
- shutil.copyfileobj(output_stream, fs)
- if changed:
- if not config.overwrite_in_place:
- tmp_file = _tmp_file(source_file)
- tmp_file.replace(source_file.path)
- if not config.quiet:
- print(f"Fixing {source_file.path}")
- finally:
- try: # Python 3.8+: use `missing_ok=True` instead of try except.
- if not config.overwrite_in_place: # pragma: no branch
- tmp_file = _tmp_file(source_file)
- tmp_file.unlink()
- except FileNotFoundError:
- pass # pragma: no cover
- else:
- changed = sort_stream(
- input_stream=source_file.stream,
- output_stream=output,
- config=config,
- file_path=actual_file_path,
- disregard_skip=disregard_skip,
- extension=extension,
- )
- if changed and show_diff:
- source_file.stream.seek(0)
- output.seek(0)
- show_unified_diff(
- file_input=source_file.stream.read(),
- file_output=output.read(),
- file_path=actual_file_path,
- output=None if show_diff is True else cast(TextIO, show_diff),
- color_output=config.color_output,
- )
- source_file.stream.close()
- except ExistingSyntaxErrors:
- warn(f"{actual_file_path} unable to sort due to existing syntax errors")
- except IntroducedSyntaxErrors: # pragma: no cover
- warn(f"{actual_file_path} unable to sort as isort introduces new syntax errors")
- return changed
- def find_imports_in_code(
- code: str,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- unique: Union[bool, ImportKey] = False,
- top_only: bool = False,
- **config_kwargs: Any,
- ) -> Iterator[identify.Import]:
- """Finds and returns all imports within the provided code string.
- - **code**: The string of code with imports that need to be sorted.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **unique**: If True, only the first instance of an import is returned.
- - **top_only**: If True, only return imports that occur before the first function or class.
- - ****config_kwargs**: Any config modifications.
- """
- yield from find_imports_in_stream(
- input_stream=StringIO(code),
- config=config,
- file_path=file_path,
- unique=unique,
- top_only=top_only,
- **config_kwargs,
- )
- def find_imports_in_stream(
- input_stream: TextIO,
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- unique: Union[bool, ImportKey] = False,
- top_only: bool = False,
- _seen: Optional[Set[str]] = None,
- **config_kwargs: Any,
- ) -> Iterator[identify.Import]:
- """Finds and returns all imports within the provided code stream.
- - **input_stream**: The stream of code with imports that need to be sorted.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **unique**: If True, only the first instance of an import is returned.
- - **top_only**: If True, only return imports that occur before the first function or class.
- - **_seen**: An optional set of imports already seen. Generally meant only for internal use.
- - ****config_kwargs**: Any config modifications.
- """
- config = _config(config=config, **config_kwargs)
- identified_imports = identify.imports(
- input_stream, config=config, file_path=file_path, top_only=top_only
- )
- if not unique:
- yield from identified_imports
- seen: Set[str] = set() if _seen is None else _seen
- for identified_import in identified_imports:
- if unique in (True, ImportKey.ALIAS):
- key = identified_import.statement()
- elif unique == ImportKey.ATTRIBUTE:
- key = f"{identified_import.module}.{identified_import.attribute}"
- elif unique == ImportKey.MODULE:
- key = identified_import.module
- elif unique == ImportKey.PACKAGE: # pragma: no branch # type checking ensures this
- key = identified_import.module.split(".")[0]
- if key and key not in seen:
- seen.add(key)
- yield identified_import
- def find_imports_in_file(
- filename: Union[str, Path],
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- unique: Union[bool, ImportKey] = False,
- top_only: bool = False,
- **config_kwargs: Any,
- ) -> Iterator[identify.Import]:
- """Finds and returns all imports within the provided source file.
- - **filename**: The name or Path of the file to look for imports in.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **unique**: If True, only the first instance of an import is returned.
- - **top_only**: If True, only return imports that occur before the first function or class.
- - ****config_kwargs**: Any config modifications.
- """
- with io.File.read(filename) as source_file:
- yield from find_imports_in_stream(
- input_stream=source_file.stream,
- config=config,
- file_path=file_path or source_file.path,
- unique=unique,
- top_only=top_only,
- **config_kwargs,
- )
- def find_imports_in_paths(
- paths: Iterator[Union[str, Path]],
- config: Config = DEFAULT_CONFIG,
- file_path: Optional[Path] = None,
- unique: Union[bool, ImportKey] = False,
- top_only: bool = False,
- **config_kwargs: Any,
- ) -> Iterator[identify.Import]:
- """Finds and returns all imports within the provided source paths.
- - **paths**: A collection of paths to recursively look for imports within.
- - **extension**: The file extension that contains imports. Defaults to filename extension or py.
- - **config**: The config object to use when sorting imports.
- - **file_path**: The disk location where the code string was pulled from.
- - **unique**: If True, only the first instance of an import is returned.
- - **top_only**: If True, only return imports that occur before the first function or class.
- - ****config_kwargs**: Any config modifications.
- """
- config = _config(config=config, **config_kwargs)
- seen: Optional[Set[str]] = set() if unique else None
- yield from chain(
- *(
- find_imports_in_file(
- file_name, unique=unique, config=config, top_only=top_only, _seen=seen
- )
- for file_name in files.find(map(str, paths), config, [], [])
- )
- )
- def _config(
- path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs: Any
- ) -> Config:
- if path and (
- config is DEFAULT_CONFIG
- and "settings_path" not in config_kwargs
- and "settings_file" not in config_kwargs
- ):
- config_kwargs["settings_path"] = path
- if config_kwargs:
- if config is not DEFAULT_CONFIG:
- raise ValueError(
- "You can either specify custom configuration options using kwargs or "
- "passing in a Config object. Not Both!"
- )
- config = Config(**config_kwargs)
- return config
|