Skip to content

Commit

Permalink
Initial work toward adding experimental support for sorting literals #…
Browse files Browse the repository at this point in the history
  • Loading branch information
timothycrosley committed Aug 2, 2020
1 parent d15c2c3 commit d35ba18
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 328 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard.
### 5.3.0 TBD
- Implemented ability to treat all or select comments as code (issue #1357)
- Implemented ability to use different configs for different file extensions (issue #1162)
- Added experimental support for sorting literals (issue #1358)
- Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363)
- Improved handling of mixed newline forms within same source file.

Expand Down
335 changes: 7 additions & 328 deletions isort/api.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import shutil
import sys
import textwrap
from io import StringIO
from itertools import chain
from pathlib import Path
from typing import List, Optional, TextIO, Union, cast
from typing import Optional, TextIO, Union, cast
from warnings import warn

from . import io, output, parse
from isort import core

from . import io
from .exceptions import (
ExistingSyntaxErrors,
FileSkipComment,
FileSkipSetting,
IntroducedSyntaxErrors,
)
from .format import (
ask_whether_to_apply_changes_to_file,
create_terminal_printer,
format_natural,
remove_whitespace,
show_unified_diff,
)
from .format import ask_whether_to_apply_changes_to_file, create_terminal_printer, show_unified_diff
from .io import Empty
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 DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config

CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport")
IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS
COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#")
from .settings import DEFAULT_CONFIG, Config


def sort_code_string(
Expand Down Expand Up @@ -164,7 +154,7 @@ def sort_stream(
_internal_output = StringIO()

try:
changed = _sort_imports(
changed = core.process(
input_stream,
_internal_output,
extension=extension or (file_path and file_path.suffix.lstrip(".")) or "py",
Expand Down Expand Up @@ -388,314 +378,3 @@ def _config(
config = Config(**config_kwargs)

return config


def _sort_imports(
input_stream: TextIO,
output_stream: TextIO,
extension: str = "py",
config: Config = DEFAULT_CONFIG,
) -> bool:
"""Parses stream identifying sections of contiguous imports and sorting them
Code with unsorted imports is read from the provided `input_stream`, sorted and then
outputted to the specified `output_stream`.
- `input_stream`: Text stream with unsorted import sections.
- `output_stream`: Text stream to output sorted inputs into.
- `config`: Config settings to use when sorting imports. Defaults settings.
- *Default*: `isort.settings.DEFAULT_CONFIG`.
- `extension`: The file extension or file extension rules that should be used.
- *Default*: `"py"`.
- *Choices*: `["py", "pyi", "pyx"]`.
Returns `True` if there were changes that needed to be made (errors present) from what
was provided in the input_stream, otherwise `False`.
"""
line_separator: str = config.line_ending
add_imports: List[str] = [format_natural(addition) for addition in config.add_imports]
import_section: str = ""
next_import_section: str = ""
next_cimports: bool = False
in_quote: str = ""
first_comment_index_start: int = -1
first_comment_index_end: int = -1
contains_imports: bool = False
in_top_comment: bool = False
first_import_section: bool = True
section_comments = [f"# {heading}" for heading in config.import_headings.values()]
indent: str = ""
isort_off: bool = False
cimports: bool = False
made_changes: bool = False

if config.float_to_top:
new_input = ""
current = ""
isort_off = False
for line in chain(input_stream, (None,)):
if isort_off and line is not None:
if line == "# isort: on\n":
isort_off = False
new_input += line
elif line in ("# isort: split\n", "# isort: off\n", None) or str(line).endswith(
"# isort: split\n"
):
if line == "# isort: off\n":
isort_off = True
if current:
parsed = parse.file_contents(current, config=config)
extra_space = ""
while current[-1] == "\n":
extra_space += "\n"
current = current[:-1]
extra_space = extra_space.replace("\n", "", 1)
sorted_output = output.sorted_imports(
parsed, config, extension, import_type="import"
)
if sorted_output.strip() != current.strip():
made_changes = True
new_input += sorted_output
new_input += extra_space
current = ""
new_input += line or ""
else:
current += line or ""

input_stream = StringIO(new_input)

for index, line in enumerate(chain(input_stream, (None,))):
if line is None:
if index == 0 and not config.force_adds:
return False

not_imports = True
line = ""
if not line_separator:
line_separator = "\n"
else:
stripped_line = line.strip()
if stripped_line and not line_separator:
line_separator = line[len(line.rstrip()) :].replace(" ", "").replace("\t", "")

for file_skip_comment in FILE_SKIP_COMMENTS:
if file_skip_comment in line:
raise FileSkipComment("Passed in content")

if (
(index == 0 or (index in (1, 2) and not contains_imports))
and stripped_line.startswith("#")
and stripped_line not in section_comments
):
in_top_comment = True
elif in_top_comment:
if not line.startswith("#") or stripped_line in section_comments:
in_top_comment = False
first_comment_index_end = index - 1

if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line:
char_index = 0
if first_comment_index_start == -1 and (
line.startswith('"') or line.startswith("'")
):
first_comment_index_start = index
while char_index < len(line):
if line[char_index] == "\\":
char_index += 1
elif in_quote:
if line[char_index : char_index + len(in_quote)] == in_quote:
in_quote = ""
if first_comment_index_end < first_comment_index_start:
first_comment_index_end = index
elif line[char_index] in ("'", '"'):
long_quote = line[char_index : char_index + 3]
if long_quote in ('"""', "'''"):
in_quote = long_quote
char_index += 2
else:
in_quote = line[char_index]
elif line[char_index] == "#":
break
char_index += 1

not_imports = bool(in_quote) or in_top_comment or isort_off
if not (in_quote or in_top_comment):
stripped_line = line.strip()
if isort_off:
if stripped_line == "# isort: on":
isort_off = False
elif stripped_line == "# isort: off":
not_imports = True
isort_off = True
elif stripped_line.endswith("# isort: split"):
not_imports = True
elif stripped_line in config.section_comments and not import_section:
import_section += line
indent = line[: -len(line.lstrip())]
elif not (stripped_line or contains_imports):
if add_imports and not indent and not config.append_only:
if not import_section:
output_stream.write(line)
line = ""
import_section += line_separator.join(add_imports) + line_separator
contains_imports = True
add_imports = []
else:
not_imports = True
elif (
not stripped_line
or stripped_line.startswith("#")
and (not indent or indent + line.lstrip() == line)
and not config.treat_all_comments_as_code
and stripped_line not in config.treat_comments_as_code
):
import_section += line
elif stripped_line.startswith(IMPORT_START_IDENTIFIERS):
contains_imports = True

new_indent = line[: -len(line.lstrip())]
import_statement = line
stripped_line = line.strip().split("#")[0]
while stripped_line.endswith("\\") or (
"(" in stripped_line and ")" not in stripped_line
):
if stripped_line.endswith("\\"):
while stripped_line and stripped_line.endswith("\\"):
line = input_stream.readline()
stripped_line = line.strip().split("#")[0]
import_statement += line
else:
while ")" not in stripped_line:
line = input_stream.readline()
stripped_line = line.strip().split("#")[0]
import_statement += line

cimport_statement: bool = False
if (
import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS)
or " cimport " in import_statement
or " cimport*" in import_statement
or " cimport(" in import_statement
or ".cimport" in import_statement
):
cimport_statement = True

if cimport_statement != cimports or (new_indent != indent and import_section):
if import_section:
next_cimports = cimport_statement
next_import_section = import_statement
import_statement = ""
not_imports = True
line = ""
else:
cimports = cimport_statement

indent = new_indent
import_section += import_statement
else:
not_imports = True

if not_imports:
raw_import_section: str = import_section
if (
add_imports
and not config.append_only
and not in_top_comment
and not in_quote
and not import_section
and not line.lstrip().startswith(COMMENT_INDICATORS)
):
import_section = line_separator.join(add_imports) + line_separator
contains_imports = True
add_imports = []

if next_import_section and not import_section: # pragma: no cover
raw_import_section = import_section = next_import_section
next_import_section = ""

if import_section:
if add_imports and not indent:
import_section = (
line_separator.join(add_imports) + line_separator + import_section
)
contains_imports = True
add_imports = []

if not indent:
import_section += line
raw_import_section += line
if not contains_imports:
output_stream.write(import_section)
else:
leading_whitespace = import_section[: -len(import_section.lstrip())]
trailing_whitespace = import_section[len(import_section.rstrip()) :]
if first_import_section and not import_section.lstrip(
line_separator
).startswith(COMMENT_INDICATORS):
import_section = import_section.lstrip(line_separator)
raw_import_section = raw_import_section.lstrip(line_separator)
first_import_section = False

if indent:
import_section = "".join(
line[len(indent) :] for line in import_section.splitlines(keepends=True)
)
out_config = Config(
config=config,
line_length=max(config.line_length - len(indent), 0),
wrap_length=max(config.wrap_length - len(indent), 0),
lines_after_imports=1,
)
else:
out_config = config

sorted_import_section = output.sorted_imports(
parse.file_contents(import_section, config=config),
out_config,
extension,
import_type="cimport" if cimports else "import",
)
if not (import_section.strip() and not sorted_import_section):
if indent:
sorted_import_section = (
leading_whitespace
+ textwrap.indent(sorted_import_section, indent).strip()
+ trailing_whitespace
)

if not made_changes:
if config.ignore_whitespace:
compare_in = remove_whitespace(
raw_import_section, line_separator=line_separator
).strip()
compare_out = remove_whitespace(
sorted_import_section, line_separator=line_separator
).strip()
else:
compare_in = raw_import_section.strip()
compare_out = sorted_import_section.strip()

if compare_out != compare_in:
made_changes = True

output_stream.write(sorted_import_section)
if not line and not indent and next_import_section:
output_stream.write(line_separator)

if indent:
output_stream.write(line)
if not next_import_section:
indent = ""

if next_import_section:
cimports = next_cimports
contains_imports = True
else:
contains_imports = False
import_section = next_import_section
next_import_section = ""
else:
output_stream.write(line)
not_imports = False

return made_changes
Loading

0 comments on commit d35ba18

Please sign in to comment.