def _rhs( self: object, line: Line, features: Collection[Feature] ) -> Iterator[Line]: """Wraps calls to `right_hand_split`. The calls increasingly `omit` right-hand trailers (bracket pairs with content), meaning the trailers get glued together to split on another bracket pair instead. """ for omit in generate_trailers_to_omit(line, mode.line_length): lines = list( right_hand_split(line, mode.line_length, features, omit=omit) ) # Note: this check is only able to figure out if the first line of the # *current* transformation fits in the line length. This is true only # for simple cases. All others require running more transforms via # `transform_line()`. This check doesn't know if those would succeed. if is_line_short_enough(lines[0], line_length=mode.line_length): yield from lines return # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition # reported as not fitting a single line, as well as lines that contain # trailing commas (those have to be exploded). yield from right_hand_split( line, line_length=mode.line_length, features=features )
def get_best_right_hand_split_without_subscripts( line: Line, features: Collection[Feature] ) -> Optional[List[Line]]: """Return a right-hand split of the given line without spliting on subscripts or None if --no-split-subscripts is disabled or there is no split that results in a short enough first line.""" if line.mode.split_subscripts: return None subscript_omit = get_omitted_subscript_bracket_ids(line) if not subscript_omit: return None for omit in generate_trailers_to_omit(line, line.mode.line_length): try: lines = list( right_hand_split( line, line.mode.line_length, features, omit=subscript_omit | omit ) ) except CannotSplit: return None if is_line_short_enough(lines[0], line_length=line.mode.line_length): return lines return None
def run_transformer( line: Line, transform: Transformer, mode: Mode, features: Collection[Feature], *, line_str: str = "", ) -> List[Line]: if not line_str: line_str = line_to_string(line) result: List[Line] = [] for transformed_line in transform(line, features): if str(transformed_line).strip("\n") == line_str: raise CannotTransform( "Line transformer returned an unchanged result") result.extend( transform_line(transformed_line, mode=mode, features=features)) if (transform.__class__.__name__ != "rhs" or not line.bracket_tracker.invisible or any(bracket.value for bracket in line.bracket_tracker.invisible) or line.contains_multiline_strings() or result[0].contains_uncollapsable_type_comments() or result[0].contains_unsplittable_type_ignore() or is_line_short_enough(result[0], line_length=mode.line_length) # If any leaves have no parents (which _can_ occur since # `transform(line)` potentially destroys the line's underlying node # structure), then we can't proceed. Doing so would cause the below # call to `append_leaves()` to fail. or any(leaf.parent is None for leaf in line.leaves)): return result line_copy = line.clone() append_leaves(line_copy, line, line.leaves) features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} second_opinion = run_transformer(line_copy, transform, mode, features_fop, line_str=line_str) if all( is_line_short_enough(ln, line_length=mode.line_length) for ln in second_opinion): result = second_opinion return result
def run_transformer( line: Line, transform: Transformer, mode: Mode, features: Collection[Feature], *, line_str: str = "", ) -> List[Line]: if not line_str: line_str = line_to_string(line) result: List[Line] = [] for transformed_line in transform(line, features): if str(transformed_line).strip("\n") == line_str: raise CannotTransform( "Line transformer returned an unchanged result") result.extend( transform_line(transformed_line, mode=mode, features=features)) if not (transform.__name__ == "rhs" and line.bracket_tracker.invisible and not any(bracket.value for bracket in line.bracket_tracker.invisible) and not line.contains_multiline_strings() and not result[0].contains_uncollapsable_type_comments() and not result[0].contains_unsplittable_type_ignore() and not is_line_short_enough(result[0], line_length=mode.line_length)): return result line_copy = line.clone() append_leaves(line_copy, line, line.leaves) features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} second_opinion = run_transformer(line_copy, transform, mode, features_fop, line_str=line_str) if all( is_line_short_enough(ln, line_length=mode.line_length) for ln in second_opinion): result = second_opinion return result
def right_hand_split( line: Line, line_length: int, features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: """Split line into many lines, starting with the last matching bracket pair. If the split was by optional parentheses, attempt splitting without them, too. `omit` is a collection of closing bracket IDs that shouldn't be considered for this split. Note: running this function modifies `bracket_depth` on the leaves of `line`. """ tail_leaves: List[Leaf] = [] body_leaves: List[Leaf] = [] head_leaves: List[Leaf] = [] current_leaves = tail_leaves opening_bracket: Optional[Leaf] = None closing_bracket: Optional[Leaf] = None for leaf in reversed(line.leaves): if current_leaves is body_leaves: if leaf is opening_bracket: current_leaves = head_leaves if body_leaves else tail_leaves current_leaves.append(leaf) if current_leaves is tail_leaves: if leaf.type in CLOSING_BRACKETS and id(leaf) not in omit: opening_bracket = leaf.opening_bracket closing_bracket = leaf current_leaves = body_leaves if not (opening_bracket and closing_bracket and head_leaves): # If there is no opening or closing_bracket that means the split failed and # all content is in the tail. Otherwise, if `head_leaves` are empty, it means # the matching `opening_bracket` wasn't available on `line` anymore. raise CannotSplit("No brackets found") tail_leaves.reverse() body_leaves.reverse() head_leaves.reverse() head = bracket_split_build_line(head_leaves, line, opening_bracket) body = bracket_split_build_line(body_leaves, line, opening_bracket, is_body=True) tail = bracket_split_build_line(tail_leaves, line, opening_bracket) bracket_split_succeeded_or_raise(head, body, tail) if ( Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren and opening_bracket.type == token.LPAR and not opening_bracket.value # the closing bracket is an optional paren and closing_bracket.type == token.RPAR and not closing_bracket.value # it's not an import (optional parens are the only thing we can split on # in this case; attempting a split without them is a waste of time) and not line.is_import # there are no standalone comments in the body and not body.contains_standalone_comments(0) # and we can actually remove the parens and can_omit_invisible_parens(body, line_length) ): omit = {id(closing_bracket), *omit} try: yield from right_hand_split(line, line_length, features=features, omit=omit) return except CannotSplit as e: if not ( can_be_split(body) or is_line_short_enough(body, line_length=line_length) ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." ) from e elif head.contains_multiline_strings() or tail.contains_multiline_strings(): raise CannotSplit( "The current optional pair of parentheses is bound to fail to" " satisfy the splitting algorithm because the head or the tail" " contains multiline strings which by definition never fit one" " line." ) from e ensure_visible(opening_bracket) ensure_visible(closing_bracket) for result in (head, body, tail): if result: yield result
def transform_line( line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: """Transform a `line`, potentially splitting it into many lines. They should fit in the allotted `line_length` but might not be able to. `features` are syntactical features that may be used in the output. """ if line.is_comment: yield line return line_str = line_to_string(line) ll = mode.line_length sn = mode.string_normalization string_merge = StringMerger(ll, sn) string_paren_strip = StringParenStripper(ll, sn) string_split = StringSplitter(ll, sn) string_paren_wrap = StringParenWrapper(ll, sn) transformers: List[Transformer] if ( not line.contains_uncollapsable_type_comments() and not line.should_split_rhs and not line.magic_trailing_comma and ( is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() ) and not (line.inside_brackets and line.contains_standalone_comments()) ): # Only apply basic string preprocessing, since lines shouldn't be split here. if Preview.string_processing in mode: transformers = [string_merge, string_paren_strip] else: transformers = [] elif line.is_def: transformers = [left_hand_split] else: def _rhs( self: object, line: Line, features: Collection[Feature] ) -> Iterator[Line]: """Wraps calls to `right_hand_split`. The calls increasingly `omit` right-hand trailers (bracket pairs with content), meaning the trailers get glued together to split on another bracket pair instead. """ for omit in generate_trailers_to_omit(line, mode.line_length): lines = list( right_hand_split(line, mode.line_length, features, omit=omit) ) # Note: this check is only able to figure out if the first line of the # *current* transformation fits in the line length. This is true only # for simple cases. All others require running more transforms via # `transform_line()`. This check doesn't know if those would succeed. if is_line_short_enough(lines[0], line_length=mode.line_length): yield from lines return # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition # reported as not fitting a single line, as well as lines that contain # trailing commas (those have to be exploded). yield from right_hand_split( line, line_length=mode.line_length, features=features ) # HACK: nested functions (like _rhs) compiled by mypyc don't retain their # __name__ attribute which is needed in `run_transformer` further down. # Unfortunately a nested class breaks mypyc too. So a class must be created # via type ... https://github.com/mypyc/mypyc/issues/884 rhs = type("rhs", (), {"__call__": _rhs})() if Preview.string_processing in mode: if line.inside_brackets: transformers = [ string_merge, string_paren_strip, string_split, delimiter_split, standalone_comment_split, string_paren_wrap, rhs, ] else: transformers = [ string_merge, string_paren_strip, string_split, string_paren_wrap, rhs, ] else: if line.inside_brackets: transformers = [delimiter_split, standalone_comment_split, rhs] else: transformers = [rhs] # It's always safe to attempt hugging of power operations and pretty much every line # could match. transformers.append(hug_power_op) for transform in transformers: # We are accumulating lines in `result` because we might want to abort # mission and return the original line in the end, or attempt a different # split altogether. try: result = run_transformer(line, transform, mode, features, line_str=line_str) except CannotTransform: continue else: yield from result break else: yield line
def transform_line( line: Line, mode: Mode, features: Collection[Feature] = ()) -> Iterator[Line]: """Transform a `line`, potentially splitting it into many lines. They should fit in the allotted `line_length` but might not be able to. `features` are syntactical features that may be used in the output. """ if line.is_comment: yield line return line_str = line_to_string(line) ll = mode.line_length sn = mode.string_normalization string_merge = StringMerger(ll, sn) string_paren_strip = StringParenStripper(ll, sn) string_split = StringSplitter(ll, sn) string_paren_wrap = StringParenWrapper(ll, sn) transformers: List[Transformer] if (not line.contains_uncollapsable_type_comments() and not line.should_split_rhs and not line.magic_trailing_comma and (is_line_short_enough( line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore()) and not (line.inside_brackets and line.contains_standalone_comments())): # Only apply basic string preprocessing, since lines shouldn't be split here. if mode.experimental_string_processing: transformers = [string_merge, string_paren_strip] else: transformers = [] elif line.is_def: transformers = [left_hand_split] else: def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: """Wraps calls to `right_hand_split`. The calls increasingly `omit` right-hand trailers (bracket pairs with content), meaning the trailers get glued together to split on another bracket pair instead. """ for omit in generate_trailers_to_omit(line, mode.line_length): lines = list( right_hand_split(line, mode.line_length, features, omit=omit)) # Note: this check is only able to figure out if the first line of the # *current* transformation fits in the line length. This is true only # for simple cases. All others require running more transforms via # `transform_line()`. This check doesn't know if those would succeed. if is_line_short_enough(lines[0], line_length=mode.line_length): yield from lines return # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition # reported as not fitting a single line, as well as lines that contain # trailing commas (those have to be exploded). yield from right_hand_split(line, line_length=mode.line_length, features=features) if mode.experimental_string_processing: if line.inside_brackets: transformers = [ string_merge, string_paren_strip, string_split, delimiter_split, standalone_comment_split, string_paren_wrap, rhs, ] else: transformers = [ string_merge, string_paren_strip, string_split, string_paren_wrap, rhs, ] else: if line.inside_brackets: transformers = [delimiter_split, standalone_comment_split, rhs] else: transformers = [rhs] for transform in transformers: # We are accumulating lines in `result` because we might want to abort # mission and return the original line in the end, or attempt a different # split altogether. try: result = run_transformer(line, transform, mode, features, line_str=line_str) except CannotTransform: continue else: yield from result break else: yield line