From 9dcaec46a0cc17ae3c0894bb1719a598e335e9a4 Mon Sep 17 00:00:00 2001 From: Jerome Leclanche Date: Fri, 14 Sep 2018 05:28:25 +0300 Subject: [PATCH] Add a --use-tabs argument to use tabs instead of spaces Closes #47, #513, #640, #2122, #2584, #2798, #3349 --- pyproject.toml | 4 +- src/black/__init__.py | 9 + src/black/linegen.py | 5 +- src/black/lines.py | 14 +- src/black/mode.py | 2 + src/black/strings.py | 7 +- tests/data/tabs/docstring_tabs.py | 276 ++++++++++++++++++++++++++++ tests/data/tabs/line_length_tabs.py | 21 +++ tests/data/tabs/long_first_line.py | 28 +++ tests/test_format.py | 13 ++ 10 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 tests/data/tabs/docstring_tabs.py create mode 100644 tests/data/tabs/line_length_tabs.py create mode 100644 tests/data/tabs/long_first_line.py diff --git a/pyproject.toml b/pyproject.toml index e7414fa8bea..32df4b669f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,8 +85,8 @@ jupyter = [ ] [project.scripts] -tan = "tan:patched_main" -tand = "tand:patched_main [d]" +tan = "black:patched_main" +tand = "blackd:patched_main [d]" [project.urls] Changelog = "https://github.com/jleclanche/tan/blob/main/CHANGES.md" diff --git a/src/black/__init__.py b/src/black/__init__.py index f24487fd398..b09dc199e3a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -281,6 +281,13 @@ def validate_regex( " functionality in the next major release." ), ) +@click.option( + "--use-tabs", + is_flag=True, + help=( + "Use tabs instead of spaces for indentation. Tabs are always equal to 4 spaces." + ), +) @click.option( "--check", is_flag=True, @@ -440,6 +447,7 @@ def main( # noqa: C901 skip_magic_trailing_comma: bool, experimental_string_processing: bool, preview: bool, + use_tabs: bool, quiet: bool, verbose: bool, required_version: Optional[str], @@ -551,6 +559,7 @@ def main( # noqa: C901 experimental_string_processing=experimental_string_processing, preview=preview, python_cell_magics=set(python_cell_magics), + use_tabs=use_tabs, ) if code is not None: diff --git a/src/black/linegen.py b/src/black/linegen.py index bfc28ca006c..52ba5eff193 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -404,10 +404,11 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote_len = 1 if docstring[1] != quote_char else 3 docstring = docstring[quote_len:-quote_len] docstring_started_empty = not docstring - indent = " " * 4 * self.current_line.depth + indent_style = " " * 4 if not self.mode.use_tabs else "\t" + indent = indent_style * self.current_line.depth if is_multiline_string(leaf): - docstring = fix_docstring(docstring, indent) + docstring = fix_docstring(docstring, indent, not self.mode.use_tabs) else: docstring = docstring.strip() diff --git a/src/black/lines.py b/src/black/lines.py index 2aa675c3b31..42e79528490 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -430,11 +430,15 @@ def clone(self) -> "Line": ) def __str__(self) -> str: + return self.render() + + def render(self, force_spaces: bool = False) -> str: """Render the line.""" if not self: return "\n" - indent = " " * self.depth + indent_style = " " if force_spaces or not self.mode.use_tabs else "\t" + indent = indent_style * self.depth leaves = iter(self.leaves) first = next(leaves) res = f"{first.prefix}{indent}{first.value}" @@ -717,7 +721,9 @@ def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> Uses the provided `line_str` rendering, if any, otherwise computes a new one. """ if not line_str: - line_str = line_to_string(line) + line_str = line_to_string(line, force_spaces=True) + else: + line_str = line_str.expandtabs(tabsize=4) return ( len(line_str) <= line_length and "\n" not in line_str # multiline strings @@ -870,9 +876,9 @@ def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool return False -def line_to_string(line: Line) -> str: +def line_to_string(line: Line, force_spaces: bool = False) -> str: """Returns the string representation of @line. WARNING: This is known to be computationally expensive. """ - return str(line).strip("\n") + return line.render(force_spaces=force_spaces).strip("\n") diff --git a/src/black/mode.py b/src/black/mode.py index 4309d4fa635..550be8c2eb6 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -188,6 +188,7 @@ class Mode: experimental_string_processing: bool = False python_cell_magics: Set[str] = field(default_factory=set) preview: bool = False + use_tabs: bool = False def __post_init__(self) -> None: if self.experimental_string_processing: @@ -229,5 +230,6 @@ def get_cache_key(self) -> str: str(int(self.experimental_string_processing)), str(int(self.preview)), sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), + str(int(self.use_tabs)), ] return ".".join(parts) diff --git a/src/black/strings.py b/src/black/strings.py index 3e3bc12fe72..7ce77d43f77 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -71,11 +71,14 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: return lines -def fix_docstring(docstring: str, prefix: str) -> str: +def fix_docstring(docstring: str, prefix: str, expand_leading_tabs: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: return "" - lines = lines_with_leading_tabs_expanded(docstring) + if expand_leading_tabs: + lines = lines_with_leading_tabs_expanded(docstring) + else: + lines = docstring.splitlines() # Determine minimum indentation (first line doesn't count): indent = sys.maxsize for line in lines[1:]: diff --git a/tests/data/tabs/docstring_tabs.py b/tests/data/tabs/docstring_tabs.py new file mode 100644 index 00000000000..9e18316a015 --- /dev/null +++ b/tests/data/tabs/docstring_tabs.py @@ -0,0 +1,276 @@ +class MyClass: + """ Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + '''This is another docstring + with more lines of text + ''' + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + '''Indentation with tabs + is just as OK + ''' + return + + +def zort(): + """Another + multiline + docstring + """ + pass + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +# output + +class MyClass: + """Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + """This is another docstring + with more lines of text + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + """Indentation with tabs + is just as OK + """ + return + + +def zort(): + """Another + multiline + docstring + """ + pass + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass diff --git a/tests/data/tabs/line_length_tabs.py b/tests/data/tabs/line_length_tabs.py new file mode 100644 index 00000000000..b86262df32c --- /dev/null +++ b/tests/data/tabs/line_length_tabs.py @@ -0,0 +1,21 @@ +print("xxxxxxxxxxx") +print("xxxxxxxxxxxx") + + +def f(): + print("xxxxxxx") + print("xxxxxxxx") + +# output + +print("xxxxxxxxxxx") +print( + "xxxxxxxxxxxx" +) + + +def f(): + print("xxxxxxx") + print( + "xxxxxxxx" + ) \ No newline at end of file diff --git a/tests/data/tabs/long_first_line.py b/tests/data/tabs/long_first_line.py new file mode 100644 index 00000000000..ea4ac41139e --- /dev/null +++ b/tests/data/tabs/long_first_line.py @@ -0,0 +1,28 @@ +__all__ = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", +] diff --git a/tests/test_format.py b/tests/test_format.py index adcbc02468d..fb9a1ae0608 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -206,3 +206,16 @@ def test_power_op_newline() -> None: # requires line_length=0 source, expected = read_data("miscellaneous", "power_op_newline") assert_format(source, expected, mode=black.Mode(line_length=0)) + + +@pytest.mark.parametrize("filename", ["long_first_line", "docstring_tabs"]) +def test_tabs(filename: str) -> None: + source, expected = read_data("tabs", filename) + mode = black.Mode(use_tabs=True) + assert_format(source, expected, mode) + + +def test_line_length_tabs() -> None: + source, expected = read_data("tabs", "line_length_tabs") + mode = black.Mode(use_tabs=True, line_length=20) + assert_format(source, expected, mode)