From 6794353fc63b72a8a61457cd29343884c109f4c3 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 --- src/black/__init__.py | 9 + src/black/linegen.py | 8 +- 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 ++ 9 files changed, 369 insertions(+), 9 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/src/black/__init__.py b/src/black/__init__.py index ded4a736822..a3ae04d91ec 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -274,6 +274,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, @@ -432,6 +439,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], @@ -533,6 +541,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 a2e41bf5912..51560f706aa 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -353,10 +353,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() @@ -453,7 +454,8 @@ def transform_line( yield line return - line_str = line_to_string(line) + # Force spaces to ensure len(line) is correct + line_str = line.render(force_spaces=True).strip("\n") ll = mode.line_length sn = mode.string_normalization diff --git a/src/black/lines.py b/src/black/lines.py index 30622650d53..39a1376e01a 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -428,11 +428,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}" @@ -650,7 +654,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 @@ -803,9 +809,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 6c0847e8bcc..9c54fae3905 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -174,6 +174,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: @@ -212,5 +213,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 9d0e2eb8430..d09fdcc699c 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -60,11 +60,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 01cd61eef63..bcb9e4720f9 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -175,3 +175,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)