From f263927d48a47c51168429353fe8ffae8166ecb0 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 17 Feb 2022 17:46:49 +0000 Subject: [PATCH 01/25] Add improved docstring processing --- CHANGES.md | 2 + docs/the_black_code_style/future_style.md | 6 + src/black/linegen.py | 2 +- src/black/strings.py | 16 +- tests/data/comments.py | 6 +- tests/data/comments_non_breaking_space.py | 10 +- ...cstring_no_string_normalization_preview.py | 241 +++++++++++ tests/data/docstring_preview.py | 380 ++++++++++++++++++ tests/data/fmtonoff.py | 6 +- tests/test_format.py | 8 + 10 files changed, 666 insertions(+), 11 deletions(-) create mode 100644 tests/data/docstring_no_string_normalization_preview.py create mode 100644 tests/data/docstring_preview.py diff --git a/CHANGES.md b/CHANGES.md index e94b345e92a..1c9107ec854 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ +- Format docstrings to have consistent quote placement + ### _Blackd_ diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 2ec2c0333a5..485d3bd5287 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -49,3 +49,9 @@ plain strings. User-made splits are respected when they do not exceed the line l limit. Line continuation backslashes are converted into parenthesized strings. Unnecessary parentheses are stripped. The stability and status of this feature is tracked in [this issue](https://github.com/psf/black/issues/2188). + +### Improved docstring processing + +_Black_ will ensure docstrings are formatted consistently, by removing extra blank lines +at the beginning and end of docstrings, ensuring the opening and closing quotes are on +their own lines and collapsing docstrings with a single line of text down to one line. diff --git a/src/black/linegen.py b/src/black/linegen.py index 4dc242a1dfe..712c3c4b3c5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -273,7 +273,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_multiline_string(leaf): indent = " " * 4 * self.current_line.depth - docstring = fix_docstring(docstring, indent) + docstring = fix_docstring(docstring, indent, self.mode.preview) else: docstring = docstring.strip() diff --git a/src/black/strings.py b/src/black/strings.py index 9d0e2eb8430..e10e94d15e7 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -60,7 +60,7 @@ 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, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: return "" @@ -81,7 +81,19 @@ def fix_docstring(docstring: str, prefix: str) -> str: trimmed.append(prefix + stripped_line) else: trimmed.append("") - return "\n".join(trimmed) + if not preview: + return "\n".join(trimmed) + # Remove extra blank lines at the ends + if all(not line or line.isspace() for line in trimmed): + return "" + for end in (0, -1): + while not trimmed[end] or trimmed[end].isspace(): + trimmed.pop(end) + trimmed[0] = prefix + trimmed[0].strip() + # Make single-line docstring single-lined + if len(trimmed) == 1: + return trimmed[0] + return "\n".join(("", *trimmed, prefix)) def get_string_prefix(string: str) -> str: diff --git a/tests/data/comments.py b/tests/data/comments.py index c34daaf6f08..fbb88e3a45a 100644 --- a/tests/data/comments.py +++ b/tests/data/comments.py @@ -4,7 +4,8 @@ # # Has many lines. Many, many lines. # Many, many, many lines. -"""Module docstring. +""" +Module docstring. Possibly also many, many lines. """ @@ -30,7 +31,8 @@ def function(default=None): - """Docstring comes first. + """ + Docstring comes first. Possibly many lines. """ diff --git a/tests/data/comments_non_breaking_space.py b/tests/data/comments_non_breaking_space.py index e17c3f4ca39..4550d8932b1 100644 --- a/tests/data/comments_non_breaking_space.py +++ b/tests/data/comments_non_breaking_space.py @@ -10,9 +10,10 @@ square = Square(4) # type: Optional[Square] def function(a:int=42): - """ This docstring is already formatted - a - b + """ + This docstring is already formatted + a + b """ #  There's a NBSP + 3 spaces before # And 4 spaces on the next line @@ -35,7 +36,8 @@ def function(a:int=42): def function(a: int = 42): - """This docstring is already formatted + """ + This docstring is already formatted a b """ diff --git a/tests/data/docstring_no_string_normalization_preview.py b/tests/data/docstring_no_string_normalization_preview.py new file mode 100644 index 00000000000..ca0b775c846 --- /dev/null +++ b/tests/data/docstring_no_string_normalization_preview.py @@ -0,0 +1,241 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +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 shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' + +# output + +class ALonelyClass: + '''A multiline class docstring.''' + + def AnEquallyLonelyMethod(self): + '''A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + ''' + This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """ + This is a docstring with + some lines of text here + """ + return + + +def baz(): + ''' + "This" is a string with some + embedded "quotes" + ''' + return + + +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 shockingly_the_quotes_are_normalized_v2(): + '''Docstring Docstring Docstring''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + '''hey there \ ''' + + +def multiline_backslash_3(): + '''already escaped \\''' diff --git a/tests/data/docstring_preview.py b/tests/data/docstring_preview.py new file mode 100644 index 00000000000..6670e3b3503 --- /dev/null +++ b/tests/data/docstring_preview.py @@ -0,0 +1,380 @@ +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 multiline_whitespace(): + ''' + + + + + ''' + + +def oneline_whitespace(): + ''' ''' + + +def empty(): + """""" + + +def single_quotes(): + 'testing' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + +def single_line_docstring_with_whitespace(): + """ This should be stripped """ + +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 + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave " + + +# 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 multiline_whitespace(): + """ """ + + +def oneline_whitespace(): + """ """ + + +def empty(): + """""" + + +def single_quotes(): + "testing" + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + '''"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def single_line_docstring_with_whitespace(): + """This should be stripped""" + + +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 + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """hey there \ """ + + +def multiline_backslash_3(): + """already escaped \\""" + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave" diff --git a/tests/data/fmtonoff.py b/tests/data/fmtonoff.py index 5a50eb12ed3..3984c7f097e 100644 --- a/tests/data/fmtonoff.py +++ b/tests/data/fmtonoff.py @@ -91,7 +91,8 @@ def example(session): .all() # fmt: on def off_and_on_without_data(): - """All comments here are technically on the same prefix. + """ + All comments here are technically on the same prefix. The comments between will be formatted. This is a known limitation. """ @@ -306,7 +307,8 @@ def example(session): def off_and_on_without_data(): - """All comments here are technically on the same prefix. + """ + All comments here are technically on the same prefix. The comments between will be formatted. This is a known limitation. """ diff --git a/tests/test_format.py b/tests/test_format.py index 04676c1c2c5..8cb9530871f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -78,6 +78,7 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", + "docstring_preview", ] SOURCES: List[str] = [ @@ -225,6 +226,13 @@ def test_docstring_no_string_normalization() -> None: assert_format(source, expected, mode) +def test_docstring_no_string_normalization_preview() -> None: + """Like test_docstring_no_string_normailiazation but with preview on.""" + source, expected = read_data("docstring_no_string_normalization_preview") + mode = replace(DEFAULT_MODE, string_normalization=False, preview=True) + assert_format(source, expected, mode) + + def test_long_strings_flag_disabled() -> None: """Tests for turning off the string processing logic.""" source, expected = read_data("long_strings_flag_disabled") From be791f57ea2be6eccbf42ee3594c2a1c87460ef5 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 17 Feb 2022 18:47:19 +0000 Subject: [PATCH 02/25] Add PR number to changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1c9107ec854..1a4f9023507 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ -- Format docstrings to have consistent quote placement +- Format docstrings to have consistent quote placement (#2885) ### _Blackd_ From 073a55d6aaee1954b914a33b795344df7a0c35d5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 20 Feb 2022 17:50:28 -0800 Subject: [PATCH 03/25] separate CHANGELOG section for preview style --- CHANGES.md | 6 +++++- docs/contributing/release_process.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e94b345e92a..310325524a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,11 @@ ### Style - + + +### Preview style + + ### _Blackd_ diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 89beb099e66..6a4b8680808 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -47,7 +47,11 @@ Use the following template for a clean changelog after the release: ### Style - + + +### Preview style + + ### _Blackd_ From 52a36b5e0a953be6fe7575aba1ee33f55ab4b415 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 21 Feb 2022 17:42:59 +0000 Subject: [PATCH 04/25] Make fix_docstring preview parameter keyword-only --- src/black/linegen.py | 2 +- src/black/strings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 712c3c4b3c5..cb5331b4f92 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -273,7 +273,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_multiline_string(leaf): indent = " " * 4 * self.current_line.depth - docstring = fix_docstring(docstring, indent, self.mode.preview) + docstring = fix_docstring(docstring, indent, preview=self.mode.preview) else: docstring = docstring.strip() diff --git a/src/black/strings.py b/src/black/strings.py index e10e94d15e7..427d9ddf5f6 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -60,7 +60,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: return lines -def fix_docstring(docstring: str, prefix: str, preview: bool) -> str: +def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: return "" From 3895c4d5d16fedfcbc26ed123043e572a9c5de72 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 21 Feb 2022 17:46:35 +0000 Subject: [PATCH 05/25] Pretend to be in preview mode for diff-shades --- src/black/strings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/strings.py b/src/black/strings.py index 427d9ddf5f6..b21045615e1 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -62,6 +62,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation + preview = True if not docstring: return "" lines = lines_with_leading_tabs_expanded(docstring) From 2541264e6d99c0ea570df95be68ad7cad6be7c73 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Mon, 21 Feb 2022 18:53:56 +0000 Subject: [PATCH 06/25] Revert "Pretend to be in preview mode for diff-shades" This reverts commit 3895c4d5d16fedfcbc26ed123043e572a9c5de72. --- src/black/strings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/black/strings.py b/src/black/strings.py index b21045615e1..427d9ddf5f6 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -62,7 +62,6 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation - preview = True if not docstring: return "" lines = lines_with_leading_tabs_expanded(docstring) From 975767f93cb025631a6ce0d3c533b476d8f5e132 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 24 Feb 2022 09:00:26 +0000 Subject: [PATCH 07/25] Blacken Black --- src/black/__init__.py | 59 ++++++++++++++++++++------------ src/black/brackets.py | 24 ++++++++----- src/black/cache.py | 9 +++-- src/black/comments.py | 18 ++++++---- src/black/concurrency.py | 4 +-- src/black/debug.py | 3 +- src/black/files.py | 15 +++++--- src/black/handle_ipynb_magics.py | 36 ++++++++++++------- src/black/linegen.py | 49 +++++++++++++++++--------- src/black/lines.py | 33 ++++++++++++------ src/black/nodes.py | 42 +++++++++++++++-------- src/black/numerics.py | 10 +++--- src/black/report.py | 6 ++-- src/black/strings.py | 6 ++-- src/black/trans.py | 24 ++++++++----- src/black_primer/lib.py | 6 ++-- tests/optional.py | 3 +- tests/test_black.py | 15 +++----- tests/test_primer.py | 12 ++++--- tests/util.py | 3 +- 20 files changed, 241 insertions(+), 136 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index c4ec99b441f..b722c9b0624 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -113,7 +113,8 @@ def from_configuration( def read_pyproject_toml( ctx: click.Context, param: click.Parameter, value: Optional[str] ) -> Optional[str]: - """Inject Black configuration from "pyproject.toml" into defaults in `ctx`. + """ + Inject Black configuration from "pyproject.toml" into defaults in `ctx`. Returns the path to a successfully found and read configuration file, None otherwise. @@ -159,7 +160,8 @@ def read_pyproject_toml( def target_version_option_callback( c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...] ) -> List[TargetVersion]: - """Compute the target versions from a --target-version flag. + """ + Compute the target versions from a --target-version flag. This is its own function because mypy couldn't infer the type correctly when it was a lambda, causing mypyc trouble. @@ -168,7 +170,8 @@ def target_version_option_callback( def re_compile_maybe_verbose(regex: str) -> Pattern[str]: - """Compile a regular expression string in `regex`. + """ + Compile a regular expression string in `regex`. If it contains newlines, use verbose mode. """ @@ -664,9 +667,7 @@ def get_sources( def path_empty( src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context ) -> None: - """ - Exit if there is no `src` provided for formatting - """ + """Exit if there is no `src` provided for formatting""" if not src: if verbose or not quiet: out(msg) @@ -700,7 +701,8 @@ def reformat_code( def reformat_one( src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report" ) -> None: - """Reformat a single file under `src` without spawning child processes. + """ + Reformat a single file under `src` without spawning child processes. `fast`, `write_back`, and `mode` options are passed to :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. @@ -803,7 +805,8 @@ async def schedule_formatting( loop: asyncio.AbstractEventLoop, executor: Executor, ) -> None: - """Run formatting of `sources` in parallel using the provided `executor`. + """ + Run formatting of `sources` in parallel using the provided `executor`. (Use ProcessPoolExecutors for actual parallelism.) @@ -875,7 +878,8 @@ def format_file_in_place( write_back: WriteBack = WriteBack.NO, lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy ) -> bool: - """Format file under `src` path. Return True if changed. + """ + Format file under `src` path. Return True if changed. If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted code to the file. @@ -934,7 +938,8 @@ def format_stdin_to_stdout( write_back: WriteBack = WriteBack.NO, mode: Mode, ) -> bool: - """Format file on stdin. Return True if changed. + """ + Format file on stdin. Return True if changed. If content is None, it's read from sys.stdin. @@ -981,7 +986,8 @@ def format_stdin_to_stdout( def check_stability_and_equivalence( src_contents: str, dst_contents: str, *, mode: Mode ) -> None: - """Perform stability and equivalence checks. + """ + Perform stability and equivalence checks. Raise AssertionError if source and destination contents are not equivalent, or if a second pass of the formatter would format the @@ -992,7 +998,8 @@ def check_stability_and_equivalence( def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: - """Reformat contents of a file and return new contents. + """ + Reformat contents of a file and return new contents. If `fast` is False, additionally confirm that the reformatted code is valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. @@ -1015,7 +1022,8 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo def validate_cell(src: str, mode: Mode) -> None: - """Check that cell does not already contain TransformerManager transformations, + """ + Check that cell does not already contain TransformerManager transformations, or non-Python cell magics, which might cause tokenizer_rt to break because of indentations. @@ -1041,7 +1049,8 @@ def validate_cell(src: str, mode: Mode) -> None: def format_cell(src: str, *, fast: bool, mode: Mode) -> str: - """Format code in given cell of Jupyter notebook. + """ + Format code in given cell of Jupyter notebook. General idea is: @@ -1078,7 +1087,8 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str: def validate_metadata(nb: MutableMapping[str, Any]) -> None: - """If notebook is marked as non-Python, don't format it. + """ + If notebook is marked as non-Python, don't format it. All notebook metadata fields are optional, see https://nbformat.readthedocs.io/en/latest/format_description.html. So @@ -1090,7 +1100,8 @@ def validate_metadata(nb: MutableMapping[str, Any]) -> None: def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: - """Format Jupyter notebook. + """ + Format Jupyter notebook. Operate cell-by-cell, only on code cells, only for Python notebooks. If the ``.ipynb`` originally had a trailing newline, it'll be preserved. @@ -1119,7 +1130,8 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon def format_str(src_contents: str, *, mode: Mode) -> str: - """Reformat a string and return new contents. + """ + Reformat a string and return new contents. `mode` determines formatting options, such as how many characters per line are allowed. Example: @@ -1146,7 +1158,6 @@ def f( arg: str = '', ) -> None: hey - """ dst_contents = _format_str_once(src_contents, mode=mode) # Forced second pass to work around optional trailing commas (becoming @@ -1188,7 +1199,8 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: - """Return a tuple of (decoded_contents, encoding, newline). + """ + Return a tuple of (decoded_contents, encoding, newline). `newline` is either CRLF or LF but `decoded_contents` is decoded with universal newlines (i.e. only contains LF). @@ -1207,7 +1219,8 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: def get_features_used( # noqa: C901 node: Node, *, future_imports: Optional[Set[str]] = None ) -> Set[Feature]: - """Return a set of (relatively) new Python features used in this file. + """ + Return a set of (relatively) new Python features used in this file. Currently looking for: - f-strings; @@ -1406,7 +1419,8 @@ def assert_stable(src: str, dst: str, mode: Mode) -> None: @contextmanager def nullcontext() -> Iterator[None]: - """Return an empty context manager. + """ + Return an empty context manager. To be used like `nullcontext` in Python 3.7. """ @@ -1414,7 +1428,8 @@ def nullcontext() -> Iterator[None]: def patch_click() -> None: - """Make Click not crash on Python 3.6 with LANG=C. + """ + Make Click not crash on Python 3.6 with LANG=C. On certain misconfigured environments, Python 3 selects the ASCII encoding as the default which restricts paths that it can access during the lifetime of the diff --git a/src/black/brackets.py b/src/black/brackets.py index c5ed4bf5b9f..592854f1747 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -66,7 +66,8 @@ class BracketTracker: invisible: List[Leaf] = field(default_factory=list) def mark(self, leaf: Leaf) -> None: - """Mark `leaf` with bracket-related metadata. Keep track of delimiters. + """ + Mark `leaf` with bracket-related metadata. Keep track of delimiters. All leaves receive an int `bracket_depth` field that stores how deep within brackets a given leaf is. 0 means there are no enclosing brackets @@ -120,7 +121,8 @@ def any_open_brackets(self) -> bool: return bool(self.bracket_match) def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: - """Return the highest priority of a delimiter found on the line. + """ + Return the highest priority of a delimiter found on the line. Values are consistent with what `is_split_*_delimiter()` return. Raises ValueError on no delimiters. @@ -128,7 +130,8 @@ def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: return max(v for k, v in self.delimiters.items() if k not in exclude) def delimiter_count_with_priority(self, priority: Priority = 0) -> int: - """Return the number of delimiters with the given `priority`. + """ + Return the number of delimiters with the given `priority`. If no `priority` is passed, defaults to max priority on the line. """ @@ -139,7 +142,8 @@ def delimiter_count_with_priority(self, priority: Priority = 0) -> int: return sum(1 for p in self.delimiters.values() if p == priority) def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool: - """In a for loop, or comprehension, the variables are often unpacks. + """ + In a for loop, or comprehension, the variables are often unpacks. To avoid splitting on the comma in this situation, increase the depth of tokens between `for` and `in`. @@ -166,7 +170,8 @@ def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool: return False def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool: - """In a lambda expression, there might be more than one argument. + """ + In a lambda expression, there might be more than one argument. To avoid splitting on the comma in this situation, increase the depth of tokens between `lambda` and `:`. @@ -197,7 +202,8 @@ def get_open_lsqb(self) -> Optional[Leaf]: def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break after it. + """ + Return the priority of the `leaf` delimiter, given a line break after it. The delimiter priorities returned here are from those delimiters that would cause a line break after themselves. @@ -211,7 +217,8 @@ def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pri def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break before it. + """ + Return the priority of the `leaf` delimiter, given a line break before it. The delimiter priorities returned here are from those delimiters that would cause a line break before themselves. @@ -307,7 +314,8 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr def max_delimiter_priority_in_atom(node: LN) -> Priority: - """Return maximum delimiter priority inside `node`. + """ + Return maximum delimiter priority inside `node`. This is specific to atoms with contents contained in a pair of parentheses. If `node` isn't an atom or there are no enclosing parentheses, returns 0. diff --git a/src/black/cache.py b/src/black/cache.py index 552c248d2ad..38c8d03ea37 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -21,7 +21,8 @@ def get_cache_dir() -> Path: - """Get the cache directory used by black. + """ + Get the cache directory used by black. Users can customize this directory on all systems using `BLACK_CACHE_DIR` environment variable. By default, the cache directory is the user cache directory @@ -40,7 +41,8 @@ def get_cache_dir() -> Path: def read_cache(mode: Mode) -> Cache: - """Read the cache if it exists and is well formed. + """ + Read the cache if it exists and is well formed. If it is not well formed, the call to write_cache later should resolve the issue. """ @@ -68,7 +70,8 @@ def get_cache_info(path: Path) -> CacheInfo: def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]: - """Split an iterable of paths in `sources` into two sets. + """ + Split an iterable of paths in `sources` into two sets. The first contains paths of files that modified on disk or are not in the cache. The other contains paths to non-modified files. diff --git a/src/black/comments.py b/src/black/comments.py index 28b9117101d..09a26a8b643 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -26,7 +26,8 @@ @dataclass class ProtoComment: - """Describes a piece of syntax that is a comment. + """ + Describes a piece of syntax that is a comment. It's not a :class:`blib2to3.pytree.Leaf` so that: @@ -43,7 +44,8 @@ class ProtoComment: def generate_comments(leaf: LN) -> Iterator[Leaf]: - """Clean the prefix of the `leaf` and generate comments from it, if any. + """ + Clean the prefix of the `leaf` and generate comments from it, if any. Comments in lib2to3 are shoved into the whitespace prefix. This happens in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation @@ -103,7 +105,8 @@ 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. + """ + Return a consistently formatted comment from the given `content` string. All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single space between the hash sign and the content. @@ -136,7 +139,8 @@ def normalize_fmt_off(node: Node) -> None: def convert_one_fmt_off_pair(node: Node) -> bool: - """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. + """ + Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. """ @@ -198,7 +202,8 @@ def convert_one_fmt_off_pair(node: Node) -> bool: def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: - """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. + """ + Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. Stops at the end of the block. @@ -236,7 +241,8 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: def is_fmt_on(container: LN) -> bool: - """Determine whether formatting is switched on within a container. + """ + Determine whether formatting is switched on within a container. Determined by whether the last `# fmt:` comment is `on` or `off`. """ fmt_on = False diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 24f67b62f06..a172e701dcb 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -7,11 +7,11 @@ def maybe_install_uvloop() -> None: - """If our environment has uvloop installed we use it. + """ + If our environment has uvloop installed we use it. This is called only from command-line entry points to avoid interfering with the parent process if Black is used as a library. - """ try: import uvloop diff --git a/src/black/debug.py b/src/black/debug.py index 5143076ab35..781e43a6ce8 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -38,7 +38,8 @@ def visit_default(self, node: LN) -> Iterator[T]: @classmethod def show(cls, code: Union[str, Leaf, Node]) -> None: - """Pretty-print the lib2to3 AST of a given string of `code`. + """ + Pretty-print the lib2to3 AST of a given string of `code`. Convenience method for debugging. """ diff --git a/src/black/files.py b/src/black/files.py index 8348e0d8c28..82032cabe86 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -32,7 +32,8 @@ @lru_cache() def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]: - """Return a directory containing .git, .hg, or pyproject.toml. + """ + Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories passed in `srcs`. @@ -95,7 +96,8 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: @mypyc_attr(patchable=True) def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: - """Parse a pyproject toml file, pulling out relevant parts for Black + """ + Parse a pyproject toml file, pulling out relevant parts for Black If parsing fails, will raise a tomli.TOMLDecodeError """ @@ -107,7 +109,8 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: @lru_cache() def find_user_pyproject_toml() -> Path: - r"""Return the path to the top-level user configuration for black. + r""" + Return the path to the top-level user configuration for black. This looks for ~\.black on Windows and ~/.config/black on Linux and other Unix systems. @@ -145,7 +148,8 @@ def normalize_path_maybe_ignore( root: Path, report: Optional[Report] = None, ) -> Optional[str]: - """Normalize `path`. May return `None` if `path` was ignored. + """ + Normalize `path`. May return `None` if `path` was ignored. `report` is where "path ignored" output goes. """ @@ -191,7 +195,8 @@ def gen_python_files( verbose: bool, quiet: bool, ) -> Iterator[Path]: - """Generate all files under `path` whose paths are not excluded by the + """ + Generate all files under `path` whose paths are not excluded by the `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, but are included by the `include` regex. diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index a0ed56baafc..5c78e52c485 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -75,7 +75,8 @@ def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: - """Remove trailing semicolon from Jupyter notebook cell. + """ + Remove trailing semicolon from Jupyter notebook cell. For example, @@ -111,7 +112,8 @@ def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: - """Put trailing semicolon back if cell originally had it. + """ + Put trailing semicolon back if cell originally had it. Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses ``tokenize_rt`` so that round-tripping works fine. @@ -135,7 +137,8 @@ def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: def mask_cell(src: str) -> Tuple[str, List[Replacement]]: - """Mask IPython magics so content becomes parseable Python code. + """ + Mask IPython magics so content becomes parseable Python code. For example, @@ -175,7 +178,8 @@ def mask_cell(src: str) -> Tuple[str, List[Replacement]]: def get_token(src: str, magic: str) -> str: - """Return randomly generated token to mask IPython magic with. + """ + Return randomly generated token to mask IPython magic with. For example, if 'magic' was `%matplotlib inline`, then a possible token to mask it with would be `"43fdd17f7e5ddc83"`. The token @@ -201,7 +205,8 @@ def get_token(src: str, magic: str) -> str: def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: - """Replace cell magic with token. + """ + Replace cell magic with token. Note that 'src' will already have been processed by IPython's TransformerManager().transform_cell. @@ -232,7 +237,8 @@ def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: def replace_magics(src: str) -> Tuple[str, List[Replacement]]: - """Replace magics within body of cell. + """ + Replace magics within body of cell. Note that 'src' will already have been processed by IPython's TransformerManager().transform_cell. @@ -273,7 +279,8 @@ def replace_magics(src: str) -> Tuple[str, List[Replacement]]: def unmask_cell(src: str, replacements: List[Replacement]) -> str: - """Remove replacements from cell. + """ + Remove replacements from cell. For example @@ -291,7 +298,8 @@ def unmask_cell(src: str, replacements: List[Replacement]) -> str: def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]: - """Check if attribute is IPython magic. + """ + Check if attribute is IPython magic. Note that the source of the abstract syntax tree will already have been processed by IPython's @@ -328,7 +336,8 @@ def header(self) -> str: # ast.NodeVisitor + dataclass = breakage under mypyc. class CellMagicFinder(ast.NodeVisitor): - """Find cell magics. + """ + Find cell magics. Note that the source of the abstract syntax tree will already have been processed by IPython's @@ -369,7 +378,8 @@ class OffsetAndMagic: # Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here # as mypyc will generate broken code. class MagicFinder(ast.NodeVisitor): - """Visit cell to look for get_ipython calls. + """ + Visit cell to look for get_ipython calls. Note that the source of the abstract syntax tree will already have been processed by IPython's @@ -391,7 +401,8 @@ def __init__(self) -> None: self.magics: Dict[int, List[OffsetAndMagic]] = collections.defaultdict(list) def visit_Assign(self, node: ast.Assign) -> None: - """Look for system assign magics. + """ + Look for system assign magics. For example, @@ -424,7 +435,8 @@ def visit_Assign(self, node: ast.Assign) -> None: self.generic_visit(node) def visit_Expr(self, node: ast.Expr) -> None: - """Look for magics in body of cell. + """ + Look for magics in body of cell. For examples, diff --git a/src/black/linegen.py b/src/black/linegen.py index cb5331b4f92..80eeb474698 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -41,7 +41,8 @@ class CannotSplit(CannotTransform): # This isn't a dataclass because @dataclass + Generic breaks mypyc. # See also https://github.com/mypyc/mypyc/issues/827. class LineGenerator(Visitor[Line]): - """Generates reformatted Line objects. Empty lines are not emitted. + """ + Generates reformatted Line objects. Empty lines are not emitted. Note: destroys the tree it's visiting by mutating prefixes of its leaves in ways that will no longer stringify to valid Python code on the tree. @@ -53,7 +54,8 @@ def __init__(self, mode: Mode) -> None: self.__post_init__() def line(self, indent: int = 0) -> Iterator[Line]: - """Generate a line. + """ + Generate a line. If the line is empty, only emit if it makes sense. If the line is too long, split it first and then generate. @@ -121,7 +123,8 @@ def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: def visit_stmt( self, node: Node, keywords: Set[str], parens: Set[str] ) -> Iterator[Line]: - """Visit a statement. + """ + Visit a statement. This implementation is shared for `if`, `while`, `for`, `try`, `except`, `def`, `with`, `class`, `assert`, and assignments. @@ -238,7 +241,8 @@ def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: yield from self.visit_default(leaf) def visit_factor(self, node: Node) -> Iterator[Line]: - """Force parentheses between a unary op and a binary power: + """ + Force parentheses between a unary op and a binary power: -2 ** 8 -> -(2 ** 8) """ @@ -332,7 +336,8 @@ def __post_init__(self) -> None: def transform_line( line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: - """Transform a `line`, potentially splitting it into many lines. + """ + Transform a `line`, potentially splitting it into many lines. They should fit in the allotted `line_length` but might not be able to. @@ -374,7 +379,8 @@ def transform_line( def _rhs( self: object, line: Line, features: Collection[Feature] ) -> Iterator[Line]: - """Wraps calls to `right_hand_split`. + """ + 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 @@ -451,7 +457,8 @@ def _rhs( def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator[Line]: - """Split line into many lines, starting with the first matching bracket pair. + """ + Split line into many lines, starting with the first matching bracket pair. Note: this usually looks weird, only use this for function definitions. Prefer RHS otherwise. This is why this function is not symmetrical with @@ -492,7 +499,8 @@ def right_hand_split( features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: - """Split line into many lines, starting with the last matching bracket pair. + """ + 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 @@ -575,7 +583,8 @@ def right_hand_split( def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None: - """Raise :exc:`CannotSplit` if the last left- or right-hand split failed. + """ + Raise :exc:`CannotSplit` if the last left- or right-hand split failed. Do nothing otherwise. @@ -603,7 +612,8 @@ def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None def bracket_split_build_line( leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False ) -> Line: - """Return a new line with given `leaves` and respective comments from `original`. + """ + Return a new line with given `leaves` and respective comments from `original`. If `is_body` is True, the result line is one-indented inside brackets and as such has its first leaf's prefix normalized and a trailing comma added when expected. @@ -658,7 +668,8 @@ def bracket_split_build_line( def dont_increase_indentation(split_func: Transformer) -> Transformer: - """Normalize prefix of the first leaf in every line returned by `split_func`. + """ + Normalize prefix of the first leaf in every line returned by `split_func`. This is a decorator over relevant split functions. """ @@ -674,7 +685,8 @@ def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Li @dont_increase_indentation def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: - """Split according to delimiters of the highest priority. + """ + Split according to delimiters of the highest priority. If the appropriate Features are given, the split will add trailing commas also in function signatures and calls that contain `*` and `**`. @@ -785,7 +797,8 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: - """Leave existing extra newlines if not `inside_brackets`. Remove everything + """ + Leave existing extra newlines if not `inside_brackets`. Remove everything else. Note: don't use backslashes for formatting or you'll lose your voting rights. @@ -803,7 +816,8 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: - """Make existing optional parentheses invisible or create new ones. + """ + Make existing optional parentheses invisible or create new ones. `parens_after` is a set of string leaf values immediately after which parens should be put. @@ -857,12 +871,12 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: - """If it's safe, make the parens in the atom `node` invisible, recursively. + """ + If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` as they are redundant. Returns whether the node should itself be wrapped in invisible parentheses. - """ if ( @@ -933,7 +947,8 @@ def should_split_line(line: Line, opening_bracket: Leaf) -> bool: def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]: - """Generate sets of closing bracket IDs that should be omitted in a RHS. + """ + Generate sets of closing bracket IDs that should be omitted in a RHS. Brackets can be omitted if the entire trailer up to and including a preceding closing bracket fits in one line. diff --git a/src/black/lines.py b/src/black/lines.py index f35665c8e0c..0da6a5f07dc 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -45,7 +45,8 @@ class Line: magic_trailing_comma: Optional[Leaf] = None def append(self, leaf: Leaf, preformatted: bool = False) -> None: - """Add a new `leaf` to the end of the line. + """ + Add a new `leaf` to the end of the line. Unless `preformatted` is True, the `leaf` will receive a new consistent whitespace prefix and metadata applied by :class:`BracketTracker`. @@ -77,7 +78,8 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: self.leaves.append(leaf) def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None: - """Like :func:`append()` but disallow invalid standalone comment structure. + """ + Like :func:`append()` but disallow invalid standalone comment structure. Raises ValueError when any `leaf` is appended after a standalone comment or when a standalone comment is not the first leaf on the line. @@ -145,7 +147,8 @@ def is_def(self) -> bool: @property def is_class_paren_empty(self) -> bool: - """Is this a class with no base classes but using parentheses? + """ + Is this a class with no base classes but using parentheses? Those are unnecessary and should be removed. """ @@ -251,7 +254,8 @@ def contains_multiline_strings(self) -> bool: def has_magic_trailing_comma( self, closing: Leaf, ensure_removable: bool = False ) -> bool: - """Return True if we have a magic trailing comma, that is when: + """ + Return True if we have a magic trailing comma, that is when: - there's a trailing comma here - it's not a one-tuple Additionally, if ensure_removable: @@ -353,7 +357,8 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: def enumerate_with_length( self, reversed: bool = False ) -> Iterator[Tuple[Index, Leaf, int]]: - """Return an enumeration of leaves with their length. + """ + Return an enumeration of leaves with their length. Stops prematurely on multiline strings and standalone comments. """ @@ -403,7 +408,8 @@ def __bool__(self) -> bool: @dataclass class EmptyLineTracker: - """Provides a stateful method that returns the number of potential extra + """ + Provides a stateful method that returns the number of potential extra empty lines needed before and after the currently processed line. Note: this tracker works on lines that haven't been split yet. It assumes @@ -417,7 +423,8 @@ class EmptyLineTracker: previous_defs: List[int] = field(default_factory=list) def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: - """Return the number of extra empty lines before and after the `current_line`. + """ + Return the number of extra empty lines before and after the `current_line`. This is for separating `def`, `async def` and `class` with extra empty lines (two on module-level). @@ -592,7 +599,8 @@ def append_leaves( def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool: - """Return True if `line` is no longer than `line_length`. + """ + Return True if `line` is no longer than `line_length`. Uses the provided `line_str` rendering, if any, otherwise computes a new one. """ @@ -606,7 +614,8 @@ def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> def can_be_split(line: Line) -> bool: - """Return False if the line cannot be split *for sure*. + """ + Return False if the line cannot be split *for sure*. This is not an exhaustive search but a cheap heuristic that we can use to avoid some unfortunate formattings (mostly around wrapping unsplittable code @@ -645,7 +654,8 @@ def can_omit_invisible_parens( line: Line, line_length: int, ) -> bool: - """Does `line` have a shape safe to reformat without optional parens around it? + """ + Does `line` have a shape safe to reformat without optional parens around it? Returns True for only a subset of potentially nice looking formattings but the point is to not return false positives that end up producing lines that @@ -751,7 +761,8 @@ def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool def line_to_string(line: Line) -> str: - """Returns the string representation of @line. + """ + Returns the string representation of @line. WARNING: This is known to be computationally expensive. """ diff --git a/src/black/nodes.py b/src/black/nodes.py index f130bff990e..1e5c2b0c93b 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -155,7 +155,8 @@ class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" def visit(self, node: LN) -> Iterator[T]: - """Main method to visit `node` and its children. + """ + Main method to visit `node` and its children. It tries to find a `visit_*()` method for the given `node.type`, like `visit_simple_stmt` for Node objects or `visit_INDENT` for Leaf objects. @@ -186,7 +187,8 @@ def visit_default(self, node: LN) -> Iterator[T]: def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 - """Return whitespace prefix if needed for the given `leaf`. + """ + Return whitespace prefix if needed for the given `leaf`. `complex_subscript` signals whether the given leaf is part of a subscription which has non-trivial arguments, like arithmetic expressions or function calls. @@ -422,10 +424,12 @@ def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool: - """Return if the `node` and its previous siblings match types against the provided + """ + Return if the `node` and its previous siblings match types against the provided list of tokens; the provided `node`has its type matched against the last element in the list. `None` can be used as the first element to declare that the start of the - list is anchored at the start of its parent's children.""" + list is anchored at the start of its parent's children. + """ if not tokens: return True if tokens[-1] is None: @@ -476,7 +480,8 @@ def replace_child(old_child: LN, new_child: LN) -> None: def container_of(leaf: Leaf) -> LN: - """Return `leaf` or one of its ancestors that is the topmost container of it. + """ + Return `leaf` or one of its ancestors that is the topmost container of it. By "container" we mean a node where `leaf` is the very first child. """ @@ -624,7 +629,8 @@ def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: def is_simple_decorator_expression(node: LN) -> bool: - """Return True iff `node` could be a 'dotted name' decorator + """ + Return True iff `node` could be a 'dotted name' decorator This function takes the node of the 'namedexpr_test' of the new decorator grammar and test if it would be valid under the old decorator grammar. @@ -669,7 +675,8 @@ def is_yield(node: LN) -> bool: def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool: - """Return True if `leaf` is a star or double star in a vararg or kwarg. + """ + Return True if `leaf` is a star or double star in a vararg or kwarg. If `within` includes VARARGS_PARENTS, this applies to function signatures. If `within` includes UNPACKING_PARENTS, it applies to right hand-side @@ -726,7 +733,8 @@ def is_stub_body(node: LN) -> bool: def is_atom_with_invisible_parens(node: LN) -> bool: - """Given a `LN`, determines whether it's an atom `node` with invisible + """ + Given a `LN`, determines whether it's an atom `node` with invisible parens. Useful in dedupe-ing and normalizing parens. """ if isinstance(node, Leaf) or node.type != syms.atom: @@ -770,15 +778,18 @@ def is_import(leaf: Leaf) -> bool: def is_type_comment(leaf: Leaf, suffix: str = "") -> bool: - """Return True if the given leaf is a special comment. - Only returns true for type comments for now.""" + """ + Return True if the given leaf is a special comment. + Only returns true for type comments for now. + """ t = leaf.type v = leaf.value return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix) def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: - """Wrap `child` in parentheses. + """ + Wrap `child` in parentheses. This replaces `child` with an atom holding the parentheses and the old child. That requires moving the prefix. @@ -796,9 +807,11 @@ def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> Non def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: - """Returns `wrapped` if `node` is of the shape ( wrapped ). + """ + Returns `wrapped` if `node` is of the shape ( wrapped ). - Parenthesis can be optional. Returns None otherwise""" + Parenthesis can be optional. Returns None otherwise + """ if len(node.children) != 3: return None @@ -810,7 +823,8 @@ def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: def ensure_visible(leaf: Leaf) -> None: - """Make sure parentheses are visible. + """ + Make sure parentheses are visible. They could be invisible as part of some statements (see :func:`normalize_invisible_parens` and :func:`visit_import_from`). diff --git a/src/black/numerics.py b/src/black/numerics.py index 879e5b2cf36..5803c48ef74 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -5,9 +5,7 @@ def format_hex(text: str) -> str: - """ - Formats a hexadecimal string like "0x12B3" - """ + """Formats a hexadecimal string like "0x12B3" """ before, after = text[:2], text[2:] return f"{before}{after.upper()}" @@ -42,9 +40,11 @@ def format_float_or_int_string(text: str) -> str: def normalize_numeric_literal(leaf: Leaf) -> None: - """Normalizes numeric (float, int, and complex) literals. + """ + Normalizes numeric (float, int, and complex) literals. - All letters used in the representation are normalized to lowercase.""" + All letters used in the representation are normalized to lowercase. + """ text = leaf.value.lower() if text.startswith(("0o", "0b")): # Leave octal and binary literals alone. diff --git a/src/black/report.py b/src/black/report.py index 43b942c9e3c..55faf29ac24 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -59,7 +59,8 @@ def path_ignored(self, path: Path, message: str) -> None: @property def return_code(self) -> int: - """Return the exit code that the app should use. + """ + Return the exit code that the app should use. This considers the current state of changed files and failures: - if there were any failures, return 123; @@ -77,7 +78,8 @@ def return_code(self) -> int: return 0 def __str__(self) -> str: - """Render a color report of the current state. + """ + Render a color report of the current state. Use `click.unstyle` to remove colors. """ diff --git a/src/black/strings.py b/src/black/strings.py index 427d9ddf5f6..9a06da191c6 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -21,7 +21,8 @@ def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: - """Replace `regex` with `replacement` twice on `original`. + """ + Replace `regex` with `replacement` twice on `original`. This is used by string normalization to perform replaces on overlapping matches. @@ -177,7 +178,8 @@ def _cached_compile(pattern: str) -> Pattern[str]: def normalize_string_quotes(s: str) -> str: - """Prefer double quotes but only if it doesn't cause more escaping. + """ + Prefer double quotes but only if it doesn't cause more escaping. Adds or removes backslashes as appropriate. Doesn't parse and fix strings nested in f-strings. diff --git a/src/black/trans.py b/src/black/trans.py index 74d052fe2dc..87cf3c0c94f 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -63,7 +63,8 @@ class CannotTransform(Exception): def TErr(err_msg: str) -> Err[CannotTransform]: - """(T)ransform Err + """ + (T)ransform Err Convenience function used when working with the TResult type. """ @@ -254,7 +255,8 @@ def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line] @dataclass class CustomSplit: - """A custom (i.e. manual) string split. + """ + A custom (i.e. manual) string split. A single CustomSplit instance represents a single substring. @@ -303,7 +305,8 @@ def _get_key(string: str) -> "CustomSplitMapMixin._Key": def add_custom_splits( self, string: str, custom_splits: Iterable[CustomSplit] ) -> None: - """Custom Split Map Setter Method + """ + Custom Split Map Setter Method Side Effects: Adds a mapping from @string to the custom splits @custom_splits. @@ -312,7 +315,8 @@ def add_custom_splits( self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits) def pop_custom_splits(self, string: str) -> List[CustomSplit]: - """Custom Split Map Getter Method + """ + Custom Split Map Getter Method Returns: * A list of the custom splits that are mapped to @string, if any @@ -341,7 +345,8 @@ def has_custom_splits(self, string: str) -> bool: class StringMerger(StringTransformer, CustomSplitMapMixin): - """StringTransformer that merges strings together. + """ + StringTransformer that merges strings together. Requirements: (A) The line contains adjacent strings such that ALL of the validation checks @@ -473,7 +478,8 @@ def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: QUOTE = LL[string_idx].value[-1] def make_naked(string: str, string_prefix: str) -> str: - """Strip @string (i.e. make it a "naked" string) + """ + Strip @string (i.e. make it a "naked" string) Pre-conditions: * assert_is_leaf_string(@string) @@ -586,7 +592,8 @@ def make_naked(string: str, string_prefix: str) -> str: @staticmethod def _validate_msg(line: Line, string_idx: int) -> TResult[None]: - """Validate (M)erge (S)tring (G)roup + """ + Validate (M)erge (S)tring (G)roup Transform-time string validation logic for __merge_string_group(...). @@ -667,7 +674,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: class StringParenStripper(StringTransformer): - """StringTransformer that strips surrounding parentheses from strings. + """ + StringTransformer that strips surrounding parentheses from strings. Requirements: The line contains a string which is surrounded by parentheses and: diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 13724f431ce..97561bb20ea 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -129,8 +129,10 @@ def analyze_results(project_count: int, results: Results) -> int: def _flatten_cli_args(cli_args: List[Union[Sequence[str], str]]) -> List[str]: - """Allow a user to put long arguments into a list of strs - to make the JSON human readable""" + """ + Allow a user to put long arguments into a list of strs + to make the JSON human readable + """ flat_args = [] for arg in cli_args: if isinstance(arg, str): diff --git a/tests/optional.py b/tests/optional.py index a4e9441ef1c..ecf745f95da 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -55,7 +55,8 @@ def pytest_addoption(parser: "Parser") -> None: def pytest_configure(config: "Config") -> None: - """Optional tests are markers. + """ + Optional tests are markers. Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks. """ diff --git a/tests/test_black.py b/tests/test_black.py index 82abd47dffd..dd97dfccb16 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1544,9 +1544,7 @@ def test_code_option_fast(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_config(self) -> None: - """ - Test that the code option finds the pyproject.toml in the current directory. - """ + """Test that the code option finds the pyproject.toml in the current directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: args = ["--code", "print"] # This is the only directory known to contain a pyproject.toml @@ -1565,9 +1563,7 @@ def test_code_option_config(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_parent_config(self) -> None: - """ - Test that the code option finds the pyproject.toml in the parent directory. - """ + """Test that the code option finds the pyproject.toml in the parent directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: with change_directory(THIS_DIR): args = ["--code", "print"] @@ -1584,9 +1580,7 @@ def test_code_option_parent_config(self) -> None: ), "Incorrect config loaded." def test_for_handled_unexpected_eof_error(self) -> None: - """ - Test that an unexpected EOF SyntaxError is nicely presented. - """ + """Test that an unexpected EOF SyntaxError is nicely presented.""" with pytest.raises(black.parsing.InvalidInput) as exc_info: black.lib2to3_parse("print(", {}) @@ -2115,7 +2109,8 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: def tracefunc( frame: types.FrameType, event: str, arg: Any ) -> Callable[[types.FrameType, str, Any], Any]: - """Show function calls `from black/__init__.py` as they happen. + """ + Show function calls `from black/__init__.py` as they happen. Register this with `sys.settrace()` in a test you're debugging. """ diff --git a/tests/test_primer.py b/tests/test_primer.py index 0a9d2aec495..daa78b4385e 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -213,8 +213,10 @@ def test_git_checkout_or_rebase(self) -> None: @patch("sys.stdout", new_callable=StringIO) @event_loop() def test_process_queue(self, mock_stdout: Mock) -> None: - """Test the process queue on primer itself - - If you have non black conforming formatting in primer itself this can fail""" + """ + Test the process queue on primer itself + - If you have non black conforming formatting in primer itself this can fail + """ loop = asyncio.get_event_loop() config_path = Path(lib.__file__).parent / "primer.json" with patch("black_primer.lib.git_checkout_or_rebase", return_false): @@ -228,8 +230,10 @@ def test_process_queue(self, mock_stdout: Mock) -> None: @event_loop() def test_load_projects_queue(self) -> None: - """Test the process queue on primer itself - - If you have non black conforming formatting in primer itself this can fail""" + """ + Test the process queue on primer itself + - If you have non black conforming formatting in primer itself this can fail + """ loop = asyncio.get_event_loop() config_path = Path(lib.__file__).parent / "primer.json" diff --git a/tests/util.py b/tests/util.py index 8755111f7c5..5221b68c445 100644 --- a/tests/util.py +++ b/tests/util.py @@ -61,7 +61,8 @@ def assert_format( fast: bool = False, minimum_version: Optional[Tuple[int, int]] = None, ) -> None: - """Convenience function to check that Black formats as expected. + """ + Convenience function to check that Black formats as expected. You can pass @minimum_version if you're passing code with newer syntax to guard safety guards so they don't just crash with a SyntaxError. Please note this is From 8adc896ff2be371e4e3c069e2b78ec9f2015d0af Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 24 Feb 2022 09:41:49 +0000 Subject: [PATCH 08/25] Shorten over-long docstrings --- tests/test_black.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index dd97dfccb16..0929a03572d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1544,7 +1544,7 @@ def test_code_option_fast(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_config(self) -> None: - """Test that the code option finds the pyproject.toml in the current directory.""" + """Test that the code option finds pyproject.toml in the current directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: args = ["--code", "print"] # This is the only directory known to contain a pyproject.toml @@ -1563,7 +1563,7 @@ def test_code_option_config(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_parent_config(self) -> None: - """Test that the code option finds the pyproject.toml in the parent directory.""" + """Test that the code option finds pyproject.toml in the parent directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: with change_directory(THIS_DIR): args = ["--code", "print"] From e5cdcff5441934079d1e793193fa96265a471be6 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Wed, 2 Mar 2022 08:26:34 +0000 Subject: [PATCH 09/25] Move opening quotes off own line --- src/black/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/strings.py b/src/black/strings.py index 9a06da191c6..58f22012169 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -94,7 +94,7 @@ def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # Make single-line docstring single-lined if len(trimmed) == 1: return trimmed[0] - return "\n".join(("", *trimmed, prefix)) + return "\n".join((*trimmed, prefix)) def get_string_prefix(string: str) -> str: From 7ed5fd2b57735a36bc6666956d14b867f8986d8c Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Wed, 2 Mar 2022 08:39:08 +0000 Subject: [PATCH 10/25] Fix first-line indentation --- src/black/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/strings.py b/src/black/strings.py index 58f22012169..dd5f819484e 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -90,7 +90,7 @@ def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: for end in (0, -1): while not trimmed[end] or trimmed[end].isspace(): trimmed.pop(end) - trimmed[0] = prefix + trimmed[0].strip() + trimmed[0] = trimmed[0].strip() # Make single-line docstring single-lined if len(trimmed) == 1: return trimmed[0] From ebb4a32992bc1dd2965223faaa1dedb03ae24751 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Wed, 2 Mar 2022 09:00:08 +0000 Subject: [PATCH 11/25] Revert "Revert "Pretend to be in preview mode for diff-shades"" This reverts commit 2541264e6d99c0ea570df95be68ad7cad6be7c73. --- src/black/strings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/strings.py b/src/black/strings.py index dd5f819484e..cb5c96f7264 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -63,6 +63,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation + preview = True if not docstring: return "" lines = lines_with_leading_tabs_expanded(docstring) From a79d3943d1b0c64de15d3270899061cfab15f122 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Wed, 2 Mar 2022 09:36:16 +0000 Subject: [PATCH 12/25] Undo temporary changes --- src/black/strings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/black/strings.py b/src/black/strings.py index cb5c96f7264..9a06da191c6 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -63,7 +63,6 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation - preview = True if not docstring: return "" lines = lines_with_leading_tabs_expanded(docstring) @@ -91,11 +90,11 @@ def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: for end in (0, -1): while not trimmed[end] or trimmed[end].isspace(): trimmed.pop(end) - trimmed[0] = trimmed[0].strip() + trimmed[0] = prefix + trimmed[0].strip() # Make single-line docstring single-lined if len(trimmed) == 1: return trimmed[0] - return "\n".join((*trimmed, prefix)) + return "\n".join(("", *trimmed, prefix)) def get_string_prefix(string: str) -> str: From 8fc0430369f07de63e8b13ecd6bb3afbc11a0fc6 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Fri, 10 Jun 2022 23:08:26 +0100 Subject: [PATCH 13/25] Fix tests --- ...ocstring_no_string_normalization_preview.py | 0 tests/data/preview/docstring_preview.py | 18 ++++++++++++------ .../docstring_preview_2.py} | 0 tests/test_format.py | 4 +++- 4 files changed, 15 insertions(+), 7 deletions(-) rename tests/data/{ => miscellaneous}/docstring_no_string_normalization_preview.py (100%) rename tests/data/{docstring_preview.py => preview/docstring_preview_2.py} (100%) diff --git a/tests/data/docstring_no_string_normalization_preview.py b/tests/data/miscellaneous/docstring_no_string_normalization_preview.py similarity index 100% rename from tests/data/docstring_no_string_normalization_preview.py rename to tests/data/miscellaneous/docstring_no_string_normalization_preview.py diff --git a/tests/data/preview/docstring_preview.py b/tests/data/preview/docstring_preview.py index 2da4cd1acdb..1f3ba32b220 100644 --- a/tests/data/preview/docstring_preview.py +++ b/tests/data/preview/docstring_preview.py @@ -56,14 +56,16 @@ def docstring_almost_at_line_limit_with_prefix(): def mulitline_docstring_almost_at_line_limit(): - """long docstring................................................................. + """ + long docstring................................................................. .................................................................................. """ def mulitline_docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................ + f""" + long docstring................................................................ .................................................................................. """ @@ -78,12 +80,16 @@ def docstring_at_line_limit_with_prefix(): def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- + """ + first line----------------------------------------------------------------------- - second line----------------------------------------------------------------------""" + second line---------------------------------------------------------------------- + """ def multiline_docstring_at_line_limit_with_prefix(): - f"""first line---------------------------------------------------------------------- + f""" + first line---------------------------------------------------------------------- - second line----------------------------------------------------------------------""" + second line---------------------------------------------------------------------- + """ diff --git a/tests/data/docstring_preview.py b/tests/data/preview/docstring_preview_2.py similarity index 100% rename from tests/data/docstring_preview.py rename to tests/data/preview/docstring_preview_2.py diff --git a/tests/test_format.py b/tests/test_format.py index feb1eb0faab..1b2ac34630a 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -180,7 +180,9 @@ def test_docstring_no_string_normalization() -> None: def test_docstring_no_string_normalization_preview() -> None: """Like test_docstring_no_string_normailiazation but with preview on.""" - source, expected = read_data("docstring_no_string_normalization_preview") + source, expected = read_data( + "miscellaneous", "docstring_no_string_normalization_preview" + ) mode = replace(DEFAULT_MODE, string_normalization=False, preview=True) assert_format(source, expected, mode) From 4b5f3c7b16ed436653792975dc41340ffcd2f89f Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Fri, 10 Jun 2022 23:08:39 +0100 Subject: [PATCH 14/25] Fix bad single-line docstring indentation --- src/black/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/strings.py b/src/black/strings.py index 9a06da191c6..0f2515d6492 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -90,10 +90,10 @@ def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: for end in (0, -1): while not trimmed[end] or trimmed[end].isspace(): trimmed.pop(end) - trimmed[0] = prefix + trimmed[0].strip() # Make single-line docstring single-lined if len(trimmed) == 1: return trimmed[0] + trimmed[0] = prefix + trimmed[0].strip() return "\n".join(("", *trimmed, prefix)) From f51ea0a93b86e565b561fe89ff64b722b10de92e Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Sat, 11 Jun 2022 08:37:59 +0100 Subject: [PATCH 15/25] Remove merge conflict residue --- docs/the_black_code_style/future_style.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index b9aa9a58cf9..d2002e68556 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -80,4 +80,3 @@ def my_func(): This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`, `while`, `with`, `case` and `match`. ->>>>>>> origin/main From a34724e5f12fc74ad7dedb84accb8a431d45c632 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Tue, 30 Aug 2022 09:16:18 +0100 Subject: [PATCH 16/25] Blacken unblackened file --- src/black/concurrency.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 48824f6907d..453b1a58e92 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -135,7 +135,8 @@ async def schedule_formatting( loop: asyncio.AbstractEventLoop, executor: "Executor", ) -> None: - """Run formatting of `sources` in parallel using the provided `executor`. + """ + Run formatting of `sources` in parallel using the provided `executor`. (Use ProcessPoolExecutors for actual parallelism.) From b8d9c2e7c059cd44cb4a7138c00c8028f3ea0202 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Fri, 2 Sep 2022 20:18:19 +0100 Subject: [PATCH 17/25] Remove rogue merge marker --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a02d00655de..7464a273b7e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,7 +69,6 @@ ### Preview style ->>>>>>> origin/main - Single-character closing docstring quotes are no longer moved to their own line as this is invalid. This was a bug introduced in version 22.6.0. (#3166) - `--skip-string-normalization` / `-S` now prevents docstring prefixes from being From 2e71f329d168108404191e88f60b3a6b3a30cbf3 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Fri, 2 Sep 2022 20:21:50 +0100 Subject: [PATCH 18/25] Add whitespace to CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 7464a273b7e..37b206784c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ ### Preview style + - Format docstrings to have consistent quote placement (#2885) ### Configuration From 3b089e3d2876971c78556389d0e021dd136d1ad0 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 27 Oct 2022 09:57:14 +0100 Subject: [PATCH 19/25] Remove extra blank line --- src/black/lines.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/black/lines.py b/src/black/lines.py index 0e4dc513b96..95f7c3986b1 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -492,7 +492,6 @@ class EmptyLineTracker: previous_defs: List[int] = field(default_factory=list) semantic_leading_comment: Optional[LinesBlock] = None - def maybe_empty_lines(self, current_line: Line) -> LinesBlock: """ Return the number of extra empty lines before and after the `current_line`. From 9c6d257c9b6e43f154699ae39959cc6c9fb167ed Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 27 Oct 2022 10:03:38 +0100 Subject: [PATCH 20/25] Put LinesBlock docstring quotes on their own line --- src/black/lines.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/lines.py b/src/black/lines.py index 95f7c3986b1..677588f8ddf 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -455,7 +455,8 @@ def __bool__(self) -> bool: @dataclass class LinesBlock: - """Class that holds information about a block of formatted lines. + """ + Class that holds information about a block of formatted lines. This is introduced so that the EmptyLineTracker can look behind the standalone comments and adjust their empty lines for class or def lines. From 9ba93f5f7a763ac222121da89ab2789ff5540c57 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 10 Nov 2022 08:13:21 +0000 Subject: [PATCH 21/25] Use new quote style --- src/black/brackets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/brackets.py b/src/black/brackets.py index e4cf260d690..feab1a0d22a 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -351,7 +351,8 @@ def max_delimiter_priority_in_atom(node: LN) -> Priority: def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]: - """Return leaves that are inside matching brackets. + """ + Return leaves that are inside matching brackets. The input `leaves` can have non-matching brackets at the head or tail parts. Matching brackets are included. From 34dbe7e8ed8257ebc8a9fcc61a847117b3ceb2e4 Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 9 Feb 2023 09:54:11 +0000 Subject: [PATCH 22/25] Fix docstring preview tests --- tests/data/{simple_cases => preview}/docstring_preview.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/data/{simple_cases => preview}/docstring_preview.py (100%) diff --git a/tests/data/simple_cases/docstring_preview.py b/tests/data/preview/docstring_preview.py similarity index 100% rename from tests/data/simple_cases/docstring_preview.py rename to tests/data/preview/docstring_preview.py From 88fe1aa1e207d63a77fdf8051e5f911a0916bf8d Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 9 Feb 2023 10:01:43 +0000 Subject: [PATCH 23/25] Blacken Black --- src/black/files.py | 12 ++++++++---- src/black/linegen.py | 10 +++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/black/files.py b/src/black/files.py index 623f3232dd6..4682a35d268 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -133,7 +133,8 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: def infer_target_version( pyproject_toml: Dict[str, Any] ) -> Optional[List[TargetVersion]]: - """Infer Black's target version from the project metadata in pyproject.toml. + """ + Infer Black's target version from the project metadata in pyproject.toml. Supports the PyPA standard format (PEP 621): https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#requires-python @@ -156,7 +157,8 @@ def infer_target_version( def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersion]]: - """Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion. + """ + Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion. If parsing fails, will raise a packaging.version.InvalidVersion error. If the parsed version cannot be mapped to a valid TargetVersion, returns None. @@ -171,7 +173,8 @@ def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersio def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVersion]]: - """Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion. + """ + Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion. If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error. If the parsed specifier cannot be mapped to a valid TargetVersion, returns None. @@ -188,7 +191,8 @@ def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVers def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: - """Strip minor versions for some specifiers in the specifier set. + """ + Strip minor versions for some specifiers in the specifier set. For background on version specifiers, see PEP 440: https://peps.python.org/pep-0440/#version-specifiers diff --git a/src/black/linegen.py b/src/black/linegen.py index 905e5539459..a7087a1116a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -694,7 +694,8 @@ def _first_right_hand_split( line: Line, omit: Collection[LeafID] = (), ) -> _RHSResult: - """Split the line into head, body, tail starting with the last bracket pair. + """ + Split the line into head, body, tail starting with the last bracket pair. Note: this function should not have side effects. It's relied upon by _maybe_split_omitting_optional_parens to get an opinion whether to prefer @@ -815,9 +816,7 @@ def _maybe_split_omitting_optional_parens( def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: - """ - Returns whether we should prefer the result from a split omitting optional parens. - """ + """Returns whether we should prefer the result from a split omitting optional parens.""" has_closing_bracket_after_assign = False for leaf in reversed(rhs_oop.head.leaves): if leaf.type == token.EQUAL: @@ -1262,7 +1261,8 @@ def remove_await_parens(node: Node) -> None: def _maybe_wrap_cms_in_parens( node: Node, mode: Mode, features: Collection[Feature] ) -> None: - """When enabled and safe, wrap the multiple context managers in invisible parens. + """ + When enabled and safe, wrap the multiple context managers in invisible parens. It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS. """ From c1f6e4a64b609961d33ed317b8cc0c32dc75ceef Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Thu, 9 Feb 2023 10:08:10 +0000 Subject: [PATCH 24/25] Shorten docstring to fit in limit --- src/black/linegen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index a7087a1116a..840aa1cac69 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -816,7 +816,7 @@ def _maybe_split_omitting_optional_parens( def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: - """Returns whether we should prefer the result from a split omitting optional parens.""" + """Return whether we should prefer a split omitting optional parens.""" has_closing_bracket_after_assign = False for leaf in reversed(rhs_oop.head.leaves): if leaf.type == token.EQUAL: From b8c169f7c662b2da07e634f6d8486af27663c76b Mon Sep 17 00:00:00 2001 From: Tom Fryers Date: Sun, 31 Mar 2024 17:34:13 +0100 Subject: [PATCH 25/25] Fix documentation link --- docs/the_black_code_style/future_style.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index afed97dd16d..1151f4be80e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -328,3 +328,5 @@ with contextlib.ExitStack() as exit_stack: _Black_ will ensure docstrings are formatted consistently, by removing extra blank lines at the beginning and end of docstrings, ensuring the opening and closing quotes are on their own lines and collapsing docstrings with a single line of text down to one line. + +(labels/preview-style)=