diff --git a/isort/literal.py b/isort/literal.py index 16ddcba0..0cce33a4 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -14,8 +14,10 @@ class ISortPrettyPrinter(PrettyPrinter): """an isort customized pretty printer for sorted literals""" - def __init__(self, config: Config): - super().__init__(width=config.line_length, compact=True) + def __init__(self, config: Config, prefix_length: int = 0): + self.config = config + self.available_width = (config.wrap_length or config.line_length) - prefix_length + super().__init__(width=self.available_width, compact=True) type_mapping: dict[str, tuple[type, Callable[[Any, ISortPrettyPrinter], str]]] = {} @@ -60,7 +62,7 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU if type(value) is not expected_type: raise LiteralSortTypeMismatch(type(value), expected_type) - printer = ISortPrettyPrinter(config) + printer = ISortPrettyPrinter(config, prefix_length=len(f"{variable_name} = ")) sorted_value_code = f"{variable_name} = {sort_function(value, printer)}" if config.formatting_function: sorted_value_code = config.formatting_function( @@ -85,31 +87,104 @@ def wrap( return wrap +def _use_vertical_hanging_indent(printer: ISortPrettyPrinter) -> bool: + return ( + printer.config.multi_line_output.name == "VERTICAL_HANGING_INDENT" + and printer.config.use_parentheses + and printer.config.include_trailing_comma + ) + + +def _format_items( + items: list[Any], + printer: ISortPrettyPrinter, + opener: str, + closer: str, + force_tuple_comma: bool = False, +) -> str: + rendered_items = [printer.pformat(item) for item in items] + single_line = f"{opener}{', '.join(rendered_items)}{closer}" + if force_tuple_comma and len(rendered_items) == 1: + single_line = f"{opener}{rendered_items[0]},{closer}" + + if not rendered_items or not _use_vertical_hanging_indent(printer): + return single_line + + if len(single_line) <= printer.available_width: + return single_line + + indent = printer.config.indent + lines = [opener] + lines.extend(f"{indent}{item}," for item in rendered_items) + lines.append(closer) + return "\n".join(lines) + + +def _format_pairs( + pairs: list[tuple[Any, Any]], + printer: ISortPrettyPrinter, + opener: str, + closer: str, +) -> str: + rendered_pairs = [f"{printer.pformat(key)}: {printer.pformat(value)}" for key, value in pairs] + single_line = f"{opener}{', '.join(rendered_pairs)}{closer}" + + if not rendered_pairs or not _use_vertical_hanging_indent(printer): + return single_line + + if len(single_line) <= printer.available_width: + return single_line + + indent = printer.config.indent + lines = [opener] + lines.extend(f"{indent}{pair}," for pair in rendered_pairs) + lines.append(closer) + return "\n".join(lines) + + @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]))) + sorted_items = sorted(value.items(), key=lambda item: item[1]) + if _use_vertical_hanging_indent(printer): + return _format_pairs(sorted_items, printer, "{", "}") + return printer.pformat(dict(sorted_items)) @register_type("list", list) def _list(value: list[Any], printer: ISortPrettyPrinter) -> str: - return printer.pformat(sorted(value)) + sorted_items = sorted(value) + if _use_vertical_hanging_indent(printer): + return _format_items(sorted_items, printer, "[", "]") + return printer.pformat(sorted_items) @register_type("unique-list", list) def _unique_list(value: list[Any], printer: ISortPrettyPrinter) -> str: - return printer.pformat(sorted(set(value))) + sorted_items = sorted(set(value)) + if _use_vertical_hanging_indent(printer): + return _format_items(sorted_items, printer, "[", "]") + return printer.pformat(sorted_items) @register_type("set", set) def _set(value: set[Any], printer: ISortPrettyPrinter) -> str: - return "{" + printer.pformat(tuple(sorted(value)))[1:-1] + "}" + sorted_items = sorted(value) + if _use_vertical_hanging_indent(printer): + return _format_items(sorted_items, printer, "{", "}") + return "{" + printer.pformat(tuple(sorted_items))[1:-1] + "}" @register_type("tuple", tuple) def _tuple(value: tuple[Any, ...], printer: ISortPrettyPrinter) -> str: - return printer.pformat(tuple(sorted(value))) + sorted_items = sorted(value) + if _use_vertical_hanging_indent(printer): + return _format_items(sorted_items, printer, "(", ")", force_tuple_comma=True) + return printer.pformat(tuple(sorted_items)) @register_type("unique-tuple", tuple) def _unique_tuple(value: tuple[Any, ...], printer: ISortPrettyPrinter) -> str: - return printer.pformat(tuple(sorted(set(value)))) + sorted_items = sorted(set(value)) + if _use_vertical_hanging_indent(printer): + return _format_items(sorted_items, printer, "(", ")", force_tuple_comma=True) + return printer.pformat(tuple(sorted_items)) diff --git a/isort/parse.py b/isort/parse.py index 03678397..2cb53447 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -400,7 +400,6 @@ def _get_next_line() -> tuple[str, str | None]: ): trailing_commas.add(import_from) else: - assert type_of_import == "straight" # noqa: S101 # Only needed for type checker if comments and attach_comments_to is not None: attach_comments_to.extend(comments) comments = [] @@ -459,14 +458,16 @@ def _get_next_line() -> tuple[str, str | None]: if placed_module and placed_module not in imports: raise MissingSection(import_module=module, section=placed_module) - straight_import |= bool( - imports[placed_module]["lazy_straight" if is_lazy else type_of_import].get( - module, False + if is_lazy: + straight_import |= bool( + imports[placed_module]["lazy_straight"].get(module, False) ) - ) - imports[placed_module]["lazy_straight" if is_lazy else type_of_import][ - module - ] = straight_import + imports[placed_module]["lazy_straight"][module] = straight_import + else: + straight_import |= bool( + imports[placed_module]["straight"].get(module, False) + ) + imports[placed_module]["straight"][module] = straight_import change_count = len(out_lines) - original_line_count diff --git a/tests/unit/test_literal.py b/tests/unit/test_literal.py index 8fa5761b..de8ae7eb 100644 --- a/tests/unit/test_literal.py +++ b/tests/unit/test_literal.py @@ -2,6 +2,7 @@ import isort.literal from isort import exceptions +from isort.settings import Config def test_value_mismatch(): @@ -26,3 +27,22 @@ def test_value_assignment_assignments(): def test_assignments_invalid_section(): with pytest.raises(exceptions.AssignmentsFormatMismatch): isort.literal.assignment("\n\nx = 1\nx++", "assignments", "py") + + +def test_tuple_sorting_uses_black_profile_wrapping(): + assert ( + isort.literal.assignment( + 'data = ("therearesuperlong", "therearesuperlong", "therearesuperlong", ' + '"therearesuperlong", "therearesuperlong")', + "tuple", + "py", + Config(profile="black"), + ) + == """data = ( + 'therearesuperlong', + 'therearesuperlong', + 'therearesuperlong', + 'therearesuperlong', + 'therearesuperlong', +)""" + )