From 1059de135c096d2c0cf281158e48f31ed82aac64 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 14 Mar 2022 09:52:57 +0000 Subject: [PATCH 1/6] dont skip formatting #%% --- CHANGES.md | 2 ++ src/black/comments.py | 4 +-- tests/data/comments3.py | 56 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index edca0dcdad4..c971746a0e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ +- Code cell separators `#%%` are now standardised to `# %%` (#2919) + ### Preview style diff --git a/src/black/comments.py b/src/black/comments.py index 28b9117101d..6b744bf217c 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -105,7 +105,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: def make_comment(content: str) -> str: """Return a consistently formatted comment from the given `content` string. - All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single + All comments (except for "##", "#!", "#:", '#'") should have a single space between the hash sign and the content. If `content` didn't start with a hash sign, one is provided. @@ -123,7 +123,7 @@ def make_comment(content: str) -> str: and not content.lstrip().startswith("type:") ): content = " " + content[1:] # Replace NBSP by a simple space - if content and content[0] not in " !:#'%": + if content and content[0] not in " !:#'": content = " " + content return "#" + content diff --git a/tests/data/comments3.py b/tests/data/comments3.py index fbbef6dcc6b..b7f150d3031 100644 --- a/tests/data/comments3.py +++ b/tests/data/comments3.py @@ -1,4 +1,6 @@ # The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. #%% def func(): @@ -44,4 +46,56 @@ def func(): ) -#%% \ No newline at end of file +#%% + +# output + +# The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. + +# %% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + + +# %% \ No newline at end of file From 8f6dcf7c0f7e043a6209a84d6dcdf07410b3a0fc Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 14 Mar 2022 16:45:13 +0000 Subject: [PATCH 2/6] use preview --- src/black/__init__.py | 4 +- src/black/comments.py | 46 ++++++++++-------- src/black/debug.py | 8 ++-- src/black/linegen.py | 82 ++++++++++++++++---------------- src/black/nodes.py | 10 ++-- tests/data/comments3.py | 56 +--------------------- tests/data/comments8.py | 101 ++++++++++++++++++++++++++++++++++++++++ tests/test_black.py | 2 +- tests/test_format.py | 1 + tests/util.py | 10 ++-- 10 files changed, 190 insertions(+), 130 deletions(-) create mode 100644 tests/data/comments8.py diff --git a/src/black/__init__.py b/src/black/__init__.py index c4ec99b441f..0e51b4b91e2 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1166,7 +1166,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: else: versions = detect_target_versions(src_node, future_imports=future_imports) - normalize_fmt_off(src_node) + normalize_fmt_off(src_node, preview=mode.preview) lines = LineGenerator(mode=mode) elt = EmptyLineTracker(is_pyi=mode.is_pyi) empty_line = Line(mode=mode) @@ -1176,7 +1176,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} if supports_feature(versions, feature) } - for current_line in lines.visit(src_node): + for current_line in lines.visit(src_node, preview=mode.preview): dst_contents.append(str(empty_line) * after) before, after = elt.maybe_empty_lines(current_line) dst_contents.append(str(empty_line) * before) diff --git a/src/black/comments.py b/src/black/comments.py index 6b744bf217c..455326469f0 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -23,6 +23,8 @@ FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} +COMMENT_EXCEPTIONS = {True: " !:#'", False: " !:#'%"} + @dataclass class ProtoComment: @@ -42,7 +44,7 @@ class ProtoComment: consumed: int # how many characters of the original leaf's prefix did we consume -def generate_comments(leaf: LN) -> Iterator[Leaf]: +def generate_comments(leaf: LN, *, preview: bool) -> Iterator[Leaf]: """Clean the prefix of the `leaf` and generate comments from it, if any. Comments in lib2to3 are shoved into the whitespace prefix. This happens @@ -61,12 +63,16 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]: Inline comments are emitted as regular token.COMMENT leaves. Standalone are emitted with a fake STANDALONE_COMMENT token identifier. """ - for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER): + for pc in list_comments( + leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, preview=preview + ): yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) @lru_cache(maxsize=4096) -def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: +def list_comments( + prefix: str, *, is_endmarker: bool, preview: bool +) -> List[ProtoComment]: """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`.""" result: List[ProtoComment] = [] if not prefix or "#" not in prefix: @@ -92,7 +98,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: comment_type = token.COMMENT # simple trailing comment else: comment_type = STANDALONE_COMMENT - comment = make_comment(line) + comment = make_comment(line, preview=preview) result.append( ProtoComment( type=comment_type, value=comment, newlines=nlines, consumed=consumed @@ -102,7 +108,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: return result -def make_comment(content: str) -> str: +def make_comment(content: str, *, preview: bool) -> str: """Return a consistently formatted comment from the given `content` string. All comments (except for "##", "#!", "#:", '#'") should have a single @@ -123,26 +129,26 @@ def make_comment(content: str) -> str: and not content.lstrip().startswith("type:") ): content = " " + content[1:] # Replace NBSP by a simple space - if content and content[0] not in " !:#'": + if content and content[0] not in COMMENT_EXCEPTIONS[preview]: content = " " + content return "#" + content -def normalize_fmt_off(node: Node) -> None: +def normalize_fmt_off(node: Node, *, preview: bool) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node) + try_again = convert_one_fmt_off_pair(node, preview=preview) -def convert_one_fmt_off_pair(node: Node) -> bool: +def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. """ for leaf in node.leaves(): previous_consumed = 0 - for comment in list_comments(leaf.prefix, is_endmarker=False): + for comment in list_comments(leaf.prefix, is_endmarker=False, preview=preview): if comment.value not in FMT_PASS: previous_consumed = comment.consumed continue @@ -157,7 +163,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: if comment.value in FMT_SKIP and prev.type in WHITESPACE: continue - ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + ignored_nodes = list(generate_ignored_nodes(leaf, comment, preview=preview)) if not ignored_nodes: continue @@ -197,7 +203,9 @@ def convert_one_fmt_off_pair(node: Node) -> bool: return False -def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: +def generate_ignored_nodes( + leaf: Leaf, comment: ProtoComment, *, preview: bool +) -> Iterator[LN]: """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. @@ -221,13 +229,13 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: yield leaf.parent return while container is not None and container.type != token.ENDMARKER: - if is_fmt_on(container): + if is_fmt_on(container, preview=preview): return # fix for fmt: on in children - if contains_fmt_on_at_column(container, leaf.column): + if contains_fmt_on_at_column(container, leaf.column, preview=preview): for child in container.children: - if contains_fmt_on_at_column(child, leaf.column): + if contains_fmt_on_at_column(child, leaf.column, preview=preview): return yield child else: @@ -235,12 +243,12 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: container = container.next_sibling -def is_fmt_on(container: LN) -> bool: +def is_fmt_on(container: LN, preview: bool) -> bool: """Determine whether formatting is switched on within a container. Determined by whether the last `# fmt:` comment is `on` or `off`. """ fmt_on = False - for comment in list_comments(container.prefix, is_endmarker=False): + for comment in list_comments(container.prefix, is_endmarker=False, preview=preview): if comment.value in FMT_ON: fmt_on = True elif comment.value in FMT_OFF: @@ -248,7 +256,7 @@ def is_fmt_on(container: LN) -> bool: return fmt_on -def contains_fmt_on_at_column(container: LN, column: int) -> bool: +def contains_fmt_on_at_column(container: LN, column: int, *, preview: bool) -> bool: """Determine if children at a given column have formatting switched on.""" for child in container.children: if ( @@ -257,7 +265,7 @@ def contains_fmt_on_at_column(container: LN, column: int) -> bool: or isinstance(child, Leaf) and child.column == column ): - if is_fmt_on(child): + if is_fmt_on(child, preview=preview): return True return False diff --git a/src/black/debug.py b/src/black/debug.py index 5143076ab35..2ca2ef11490 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -16,14 +16,14 @@ class DebugVisitor(Visitor[T]): tree_depth: int = 0 - def visit_default(self, node: LN) -> Iterator[T]: + def visit_default(self, node: LN, *, preview: bool) -> Iterator[T]: indent = " " * (2 * self.tree_depth) if isinstance(node, Node): _type = type_repr(node.type) out(f"{indent}{_type}", fg="yellow") self.tree_depth += 1 for child in node.children: - yield from self.visit(child) + yield from self.visit(child, preview=preview) self.tree_depth -= 1 out(f"{indent}/{_type}", fg="yellow", bold=False) @@ -37,7 +37,7 @@ def visit_default(self, node: LN) -> Iterator[T]: out(f" {node.value!r}", fg="blue", bold=False) @classmethod - def show(cls, code: Union[str, Leaf, Node]) -> None: + def show(cls, code: Union[str, Leaf, Node], *, preview: bool) -> None: """Pretty-print the lib2to3 AST of a given string of `code`. Convenience method for debugging. @@ -45,4 +45,4 @@ def show(cls, code: Union[str, Leaf, Node]) -> None: v: DebugVisitor[None] = DebugVisitor() if isinstance(code, str): code = lib2to3_parse(code) - list(v.visit(code)) + list(v.visit(code, preview=preview)) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4dc242a1dfe..6a671d63676 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -68,11 +68,11 @@ def line(self, indent: int = 0) -> Iterator[Line]: self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) yield complete_line - def visit_default(self, node: LN) -> Iterator[Line]: + def visit_default(self, node: LN, *, preview: bool) -> Iterator[Line]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Leaf): any_open_brackets = self.current_line.bracket_tracker.any_open_brackets() - for comment in generate_comments(node): + for comment in generate_comments(node, preview=preview): if any_open_brackets: # any comment within brackets is subject to splitting self.current_line.append(comment) @@ -96,15 +96,15 @@ def visit_default(self, node: LN) -> Iterator[Line]: normalize_numeric_literal(node) if node.type not in WHITESPACE: self.current_line.append(node) - yield from super().visit_default(node) + yield from super().visit_default(node, preview=preview) - def visit_INDENT(self, node: Leaf) -> Iterator[Line]: + def visit_INDENT(self, node: Leaf, *, preview: bool) -> Iterator[Line]: """Increase indentation level, maybe yield a line.""" # In blib2to3 INDENT never holds comments. yield from self.line(+1) - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: + def visit_DEDENT(self, node: Leaf, *, preview: bool) -> Iterator[Line]: """Decrease indentation level, maybe yield a line.""" # The current line might still wait for trailing comments. At DEDENT time # there won't be any (they would be prefixes on the preceding NEWLINE). @@ -113,13 +113,13 @@ def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: # While DEDENT has no value, its prefix may contain standalone comments # that belong to the current indentation level. Get 'em. - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) # Finally, emit the dedent. yield from self.line(-1) def visit_stmt( - self, node: Node, keywords: Set[str], parens: Set[str] + self, node: Node, keywords: Set[str], parens: Set[str], *, preview: bool ) -> Iterator[Line]: """Visit a statement. @@ -132,29 +132,29 @@ def visit_stmt( `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens) + normalize_invisible_parens(node, parens_after=parens, preview=preview) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() - yield from self.visit(child) + yield from self.visit(child, preview=preview) - def visit_match_case(self, node: Node) -> Iterator[Line]: + def visit_match_case(self, node: Node, *, preview: bool) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set()) + normalize_invisible_parens(node, parens_after=set(), preview=preview) yield from self.line() for child in node.children: - yield from self.visit(child) + yield from self.visit(child, preview=preview) - def visit_suite(self, node: Node) -> Iterator[Line]: + def visit_suite(self, node: Node, *, preview: bool) -> Iterator[Line]: """Visit a suite.""" if self.mode.is_pyi and is_stub_suite(node): - yield from self.visit(node.children[2]) + yield from self.visit(node.children[2], preview=preview) else: - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_simple_stmt(self, node: Node) -> Iterator[Line]: + def visit_simple_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: """Visit a statement without nested statements.""" prev_type: Optional[int] = None for child in node.children: @@ -165,10 +165,10 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: if self.mode.is_pyi and is_stub_body(node): - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) else: yield from self.line(+1) - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) yield from self.line(-1) else: @@ -178,30 +178,30 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: or not is_stub_suite(node.parent) ): yield from self.line() - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_async_stmt(self, node: Node) -> Iterator[Line]: + def visit_async_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: """Visit `async def`, `async for`, `async with`.""" yield from self.line() children = iter(node.children) for child in children: - yield from self.visit(child) + yield from self.visit(child, preview=preview) if child.type == token.ASYNC: break internal_stmt = next(children) for child in internal_stmt.children: - yield from self.visit(child) + yield from self.visit(child, preview=preview) - def visit_decorators(self, node: Node) -> Iterator[Line]: + def visit_decorators(self, node: Node, *, preview: bool) -> Iterator[Line]: """Visit decorators.""" for child in node.children: yield from self.line() - yield from self.visit(child) + yield from self.visit(child, preview=preview) - def visit_power(self, node: Node) -> Iterator[Line]: + def visit_power(self, node: Node, *, preview: bool) -> Iterator[Line]: for idx, leaf in enumerate(node.children[:-1]): next_leaf = node.children[idx + 1] @@ -221,23 +221,23 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: + def visit_SEMI(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: """Remove a semicolon and put the other statement on a separate line.""" yield from self.line() - def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]: + def visit_ENDMARKER(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: """End of file. Process outstanding comments and end with a newline.""" - yield from self.visit_default(leaf) + yield from self.visit_default(leaf, preview=preview) yield from self.line() - def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: + def visit_STANDALONE_COMMENT(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: if not self.current_line.bracket_tracker.any_open_brackets(): yield from self.line() - yield from self.visit_default(leaf) + yield from self.visit_default(leaf, preview=preview) - def visit_factor(self, node: Node) -> Iterator[Line]: + def visit_factor(self, node: Node, *, preview: bool) -> Iterator[Line]: """Force parentheses between a unary op and a binary power: -2 ** 8 -> -(2 ** 8) @@ -252,9 +252,9 @@ def visit_factor(self, node: Node) -> Iterator[Line]: rpar = Leaf(token.RPAR, ")") index = operand.remove() or 0 node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: + def visit_STRING(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. @@ -296,7 +296,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote = quote_char * quote_len leaf.value = prefix + quote + docstring + quote - yield from self.visit_default(leaf) + yield from self.visit_default(leaf, preview=preview) def __post_init__(self) -> None: """You are in a twisty little maze of passages.""" @@ -802,7 +802,9 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: leaf.prefix = "" -def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: +def normalize_invisible_parens( + node: Node, parens_after: Set[str], *, preview: bool +) -> None: """Make existing optional parentheses invisible or create new ones. `parens_after` is a set of string leaf values immediately after which parens @@ -811,7 +813,7 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. """ - for pc in list_comments(node.prefix, is_endmarker=False): + for pc in list_comments(node.prefix, is_endmarker=False, preview=preview): if pc.value in FMT_OFF: # This `node` has a prefix with `# fmt: off`, don't mess with parens. return @@ -820,7 +822,9 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: # Fixes a bug where invisible parens are not properly stripped from # assignment statements that contain type annotations. if isinstance(child, Node) and child.type == syms.annassign: - normalize_invisible_parens(child, parens_after=parens_after) + normalize_invisible_parens( + child, parens_after=parens_after, preview=preview + ) # Add parentheses around long tuple unpacking in assignments. if ( diff --git a/src/black/nodes.py b/src/black/nodes.py index f130bff990e..efd4a3173f6 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -154,7 +154,7 @@ class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" - def visit(self, node: LN) -> Iterator[T]: + def visit(self, node: LN, *, preview: bool) -> Iterator[T]: """Main method to visit `node` and its children. It tries to find a `visit_*()` method for the given `node.type`, like @@ -174,15 +174,15 @@ def visit(self, node: LN) -> Iterator[T]: # generate a native call to visit_default. visitf = getattr(self, f"visit_{name}", None) if visitf: - yield from visitf(node) + yield from visitf(node, preview=preview) else: - yield from self.visit_default(node) + yield from self.visit_default(node, preview=preview) - def visit_default(self, node: LN) -> Iterator[T]: + def visit_default(self, node: LN, *, preview: bool) -> Iterator[T]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Node): for child in node.children: - yield from self.visit(child) + yield from self.visit(child, preview=preview) def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 diff --git a/tests/data/comments3.py b/tests/data/comments3.py index b7f150d3031..fbbef6dcc6b 100644 --- a/tests/data/comments3.py +++ b/tests/data/comments3.py @@ -1,6 +1,4 @@ # The percent-percent comments are Spyder IDE cells. -# Both `#%%`` and `# %%` are accepted, so `black` standardises -# to the latter. #%% def func(): @@ -46,56 +44,4 @@ def func(): ) -#%% - -# output - -# The percent-percent comments are Spyder IDE cells. -# Both `#%%`` and `# %%` are accepted, so `black` standardises -# to the latter. - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - # This should be left alone (after) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% \ No newline at end of file +#%% \ No newline at end of file diff --git a/tests/data/comments8.py b/tests/data/comments8.py new file mode 100644 index 00000000000..b7f150d3031 --- /dev/null +++ b/tests/data/comments8.py @@ -0,0 +1,101 @@ +# The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. + +#%% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + + +#%% + +# output + +# The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. + +# %% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + + +# %% \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index b1bf1772550..c6aaed3c3d8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -857,7 +857,7 @@ def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) with patch("black.debug.out", out): - DebugVisitor.show(source) + DebugVisitor.show(source, preview=True) actual = "\n".join(out_lines) + "\n" log_name = "" if expected != actual: diff --git a/tests/test_format.py b/tests/test_format.py index 04eda43d5cf..2febb41b291 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -75,6 +75,7 @@ # string processing "cantfit", "comments7", + "comments8", "long_strings", "long_strings__edge_case", "long_strings__regression", diff --git a/tests/util.py b/tests/util.py index 8755111f7c5..104370960e0 100644 --- a/tests/util.py +++ b/tests/util.py @@ -29,21 +29,21 @@ fs = partial(black.format_str, mode=DEFAULT_MODE) -def _assert_format_equal(expected: str, actual: str) -> None: +def _assert_format_equal(expected: str, actual: str, *, preview: bool) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): bdv: DebugVisitor[Any] out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) bdv = DebugVisitor() - list(bdv.visit(exp_node)) + list(bdv.visit(exp_node, preview=preview)) except Exception as ve: err(str(ve)) out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) bdv = DebugVisitor() - list(bdv.visit(exp_node)) + list(bdv.visit(exp_node, preview=preview)) except Exception as ve: err(str(ve)) @@ -68,7 +68,7 @@ def assert_format( separate from TargetVerson Mode configuration. """ actual = black.format_str(source, mode=mode) - _assert_format_equal(expected, actual) + _assert_format_equal(expected, actual, preview=mode.preview) # It's not useful to run safety checks if we're expecting no changes anyway. The # assertion right above will raise if reality does actually make changes. This just # avoids wasted CPU cycles. @@ -87,7 +87,7 @@ def dump_to_stderr(*output: str) -> str: class BlackBaseTestCase(unittest.TestCase): def assertFormatEqual(self, expected: str, actual: str) -> None: - _assert_format_equal(expected, actual) + _assert_format_equal(expected, actual, preview=True) def read_data(name: str, data: bool = True) -> Tuple[str, str]: From 71ba4a7996f0a3b1c1481bafb62bb14e610d7f64 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 14 Mar 2022 16:54:26 +0000 Subject: [PATCH 3/6] fixup --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c971746a0e5..dad60053a88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,12 +10,12 @@ -- Code cell separators `#%%` are now standardised to `# %%` (#2919) - ### Preview style +- Code cell separators `#%%` are now standardised to `# %%` (#2919) + ### _Blackd_ From 5f612b783050f6a986959180200c8122ace94a0b Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 14 Mar 2022 17:00:01 +0000 Subject: [PATCH 4/6] use preview=False in BlackBaseTestCase --- tests/test_black.py | 2 +- tests/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index c6aaed3c3d8..d4584e7b118 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -857,7 +857,7 @@ def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) with patch("black.debug.out", out): - DebugVisitor.show(source, preview=True) + DebugVisitor.show(source, preview=False) actual = "\n".join(out_lines) + "\n" log_name = "" if expected != actual: diff --git a/tests/util.py b/tests/util.py index 104370960e0..0ab4ba6dd36 100644 --- a/tests/util.py +++ b/tests/util.py @@ -87,7 +87,7 @@ def dump_to_stderr(*output: str) -> str: class BlackBaseTestCase(unittest.TestCase): def assertFormatEqual(self, expected: str, actual: str) -> None: - _assert_format_equal(expected, actual, preview=True) + _assert_format_equal(expected, actual, preview=False) def read_data(name: str, data: bool = True) -> Tuple[str, str]: From 22a0d086ef28f6336c97df16738e5950b8a12426 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Tue, 15 Mar 2022 13:30:36 +0000 Subject: [PATCH 5/6] use self.mode.preview --- src/black/__init__.py | 2 +- src/black/debug.py | 8 ++--- src/black/linegen.py | 72 +++++++++++++++++++++---------------------- src/black/nodes.py | 10 +++--- tests/test_black.py | 2 +- tests/util.py | 10 +++--- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 0e51b4b91e2..51e31e9598b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1176,7 +1176,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} if supports_feature(versions, feature) } - for current_line in lines.visit(src_node, preview=mode.preview): + for current_line in lines.visit(src_node): dst_contents.append(str(empty_line) * after) before, after = elt.maybe_empty_lines(current_line) dst_contents.append(str(empty_line) * before) diff --git a/src/black/debug.py b/src/black/debug.py index 2ca2ef11490..5143076ab35 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -16,14 +16,14 @@ class DebugVisitor(Visitor[T]): tree_depth: int = 0 - def visit_default(self, node: LN, *, preview: bool) -> Iterator[T]: + def visit_default(self, node: LN) -> Iterator[T]: indent = " " * (2 * self.tree_depth) if isinstance(node, Node): _type = type_repr(node.type) out(f"{indent}{_type}", fg="yellow") self.tree_depth += 1 for child in node.children: - yield from self.visit(child, preview=preview) + yield from self.visit(child) self.tree_depth -= 1 out(f"{indent}/{_type}", fg="yellow", bold=False) @@ -37,7 +37,7 @@ def visit_default(self, node: LN, *, preview: bool) -> Iterator[T]: out(f" {node.value!r}", fg="blue", bold=False) @classmethod - def show(cls, code: Union[str, Leaf, Node], *, preview: bool) -> None: + def show(cls, code: Union[str, Leaf, Node]) -> None: """Pretty-print the lib2to3 AST of a given string of `code`. Convenience method for debugging. @@ -45,4 +45,4 @@ def show(cls, code: Union[str, Leaf, Node], *, preview: bool) -> None: v: DebugVisitor[None] = DebugVisitor() if isinstance(code, str): code = lib2to3_parse(code) - list(v.visit(code, preview=preview)) + list(v.visit(code)) diff --git a/src/black/linegen.py b/src/black/linegen.py index 6a671d63676..79475a83f0e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -68,11 +68,11 @@ def line(self, indent: int = 0) -> Iterator[Line]: self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) yield complete_line - def visit_default(self, node: LN, *, preview: bool) -> Iterator[Line]: + def visit_default(self, node: LN) -> Iterator[Line]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Leaf): any_open_brackets = self.current_line.bracket_tracker.any_open_brackets() - for comment in generate_comments(node, preview=preview): + for comment in generate_comments(node, preview=self.mode.preview): if any_open_brackets: # any comment within brackets is subject to splitting self.current_line.append(comment) @@ -96,15 +96,15 @@ def visit_default(self, node: LN, *, preview: bool) -> Iterator[Line]: normalize_numeric_literal(node) if node.type not in WHITESPACE: self.current_line.append(node) - yield from super().visit_default(node, preview=preview) + yield from super().visit_default(node) - def visit_INDENT(self, node: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_INDENT(self, node: Leaf) -> Iterator[Line]: """Increase indentation level, maybe yield a line.""" # In blib2to3 INDENT never holds comments. yield from self.line(+1) - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_DEDENT(self, node: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: """Decrease indentation level, maybe yield a line.""" # The current line might still wait for trailing comments. At DEDENT time # there won't be any (they would be prefixes on the preceding NEWLINE). @@ -113,13 +113,13 @@ def visit_DEDENT(self, node: Leaf, *, preview: bool) -> Iterator[Line]: # While DEDENT has no value, its prefix may contain standalone comments # that belong to the current indentation level. Get 'em. - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) # Finally, emit the dedent. yield from self.line(-1) def visit_stmt( - self, node: Node, keywords: Set[str], parens: Set[str], *, preview: bool + self, node: Node, keywords: Set[str], parens: Set[str] ) -> Iterator[Line]: """Visit a statement. @@ -132,29 +132,29 @@ def visit_stmt( `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens, preview=preview) + normalize_invisible_parens(node, parens_after=parens, preview=self.mode.preview) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() - yield from self.visit(child, preview=preview) + yield from self.visit(child) - def visit_match_case(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set(), preview=preview) + normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) yield from self.line() for child in node.children: - yield from self.visit(child, preview=preview) + yield from self.visit(child) - def visit_suite(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" if self.mode.is_pyi and is_stub_suite(node): - yield from self.visit(node.children[2], preview=preview) + yield from self.visit(node.children[2]) else: - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_simple_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_simple_stmt(self, node: Node) -> Iterator[Line]: """Visit a statement without nested statements.""" prev_type: Optional[int] = None for child in node.children: @@ -165,10 +165,10 @@ def visit_simple_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: if self.mode.is_pyi and is_stub_body(node): - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) else: yield from self.line(+1) - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) yield from self.line(-1) else: @@ -178,30 +178,30 @@ def visit_simple_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: or not is_stub_suite(node.parent) ): yield from self.line() - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_async_stmt(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_async_stmt(self, node: Node) -> Iterator[Line]: """Visit `async def`, `async for`, `async with`.""" yield from self.line() children = iter(node.children) for child in children: - yield from self.visit(child, preview=preview) + yield from self.visit(child) if child.type == token.ASYNC: break internal_stmt = next(children) for child in internal_stmt.children: - yield from self.visit(child, preview=preview) + yield from self.visit(child) - def visit_decorators(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_decorators(self, node: Node) -> Iterator[Line]: """Visit decorators.""" for child in node.children: yield from self.line() - yield from self.visit(child, preview=preview) + yield from self.visit(child) - def visit_power(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_power(self, node: Node) -> Iterator[Line]: for idx, leaf in enumerate(node.children[:-1]): next_leaf = node.children[idx + 1] @@ -221,23 +221,23 @@ def visit_power(self, node: Node, *, preview: bool) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_SEMI(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: """Remove a semicolon and put the other statement on a separate line.""" yield from self.line() - def visit_ENDMARKER(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]: """End of file. Process outstanding comments and end with a newline.""" - yield from self.visit_default(leaf, preview=preview) + yield from self.visit_default(leaf) yield from self.line() - def visit_STANDALONE_COMMENT(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: if not self.current_line.bracket_tracker.any_open_brackets(): yield from self.line() - yield from self.visit_default(leaf, preview=preview) + yield from self.visit_default(leaf) - def visit_factor(self, node: Node, *, preview: bool) -> Iterator[Line]: + def visit_factor(self, node: Node) -> Iterator[Line]: """Force parentheses between a unary op and a binary power: -2 ** 8 -> -(2 ** 8) @@ -252,9 +252,9 @@ def visit_factor(self, node: Node, *, preview: bool) -> Iterator[Line]: rpar = Leaf(token.RPAR, ")") index = operand.remove() or 0 node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_STRING(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: + def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. @@ -296,7 +296,7 @@ def visit_STRING(self, leaf: Leaf, *, preview: bool) -> Iterator[Line]: quote = quote_char * quote_len leaf.value = prefix + quote + docstring + quote - yield from self.visit_default(leaf, preview=preview) + yield from self.visit_default(leaf) def __post_init__(self) -> None: """You are in a twisty little maze of passages.""" diff --git a/src/black/nodes.py b/src/black/nodes.py index efd4a3173f6..f130bff990e 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -154,7 +154,7 @@ class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" - def visit(self, node: LN, *, preview: bool) -> Iterator[T]: + def visit(self, node: LN) -> Iterator[T]: """Main method to visit `node` and its children. It tries to find a `visit_*()` method for the given `node.type`, like @@ -174,15 +174,15 @@ def visit(self, node: LN, *, preview: bool) -> Iterator[T]: # generate a native call to visit_default. visitf = getattr(self, f"visit_{name}", None) if visitf: - yield from visitf(node, preview=preview) + yield from visitf(node) else: - yield from self.visit_default(node, preview=preview) + yield from self.visit_default(node) - def visit_default(self, node: LN, *, preview: bool) -> Iterator[T]: + def visit_default(self, node: LN) -> Iterator[T]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Node): for child in node.children: - yield from self.visit(child, preview=preview) + yield from self.visit(child) def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 diff --git a/tests/test_black.py b/tests/test_black.py index d4584e7b118..b1bf1772550 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -857,7 +857,7 @@ def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) with patch("black.debug.out", out): - DebugVisitor.show(source, preview=False) + DebugVisitor.show(source) actual = "\n".join(out_lines) + "\n" log_name = "" if expected != actual: diff --git a/tests/util.py b/tests/util.py index 0ab4ba6dd36..8755111f7c5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -29,21 +29,21 @@ fs = partial(black.format_str, mode=DEFAULT_MODE) -def _assert_format_equal(expected: str, actual: str, *, preview: bool) -> None: +def _assert_format_equal(expected: str, actual: str) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): bdv: DebugVisitor[Any] out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) bdv = DebugVisitor() - list(bdv.visit(exp_node, preview=preview)) + list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) bdv = DebugVisitor() - list(bdv.visit(exp_node, preview=preview)) + list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) @@ -68,7 +68,7 @@ def assert_format( separate from TargetVerson Mode configuration. """ actual = black.format_str(source, mode=mode) - _assert_format_equal(expected, actual, preview=mode.preview) + _assert_format_equal(expected, actual) # It's not useful to run safety checks if we're expecting no changes anyway. The # assertion right above will raise if reality does actually make changes. This just # avoids wasted CPU cycles. @@ -87,7 +87,7 @@ def dump_to_stderr(*output: str) -> str: class BlackBaseTestCase(unittest.TestCase): def assertFormatEqual(self, expected: str, actual: str) -> None: - _assert_format_equal(expected, actual, preview=False) + _assert_format_equal(expected, actual) def read_data(name: str, data: bool = True) -> Tuple[str, str]: From e8ee4d5ef8811bafe4caaee37c3d1efb4bf4cf32 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 21 Mar 2022 21:05:24 +0000 Subject: [PATCH 6/6] shorten test file --- tests/data/comments8.py | 90 +---------------------------------------- 1 file changed, 2 insertions(+), 88 deletions(-) diff --git a/tests/data/comments8.py b/tests/data/comments8.py index b7f150d3031..a2030c2a092 100644 --- a/tests/data/comments8.py +++ b/tests/data/comments8.py @@ -3,50 +3,7 @@ # to the latter. #%% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - # This should be left alone (after) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -#%% +# %% # output @@ -55,47 +12,4 @@ def func(): # to the latter. # %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - # This should be left alone (after) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% \ No newline at end of file +# %%