From 6e97c5f47cbec72c72c27aefb206589dd84707a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Fri, 21 Jan 2022 01:42:07 +0200 Subject: [PATCH 01/50] Deprecate ESP and move the functionality under --preview (#2789) --- CHANGES.md | 5 ++++- docs/faq.md | 2 +- docs/the_black_code_style/current_style.md | 14 ++++--------- docs/the_black_code_style/future_style.md | 19 +++++++++-------- src/black/__init__.py | 5 +---- src/black/linegen.py | 7 +++---- src/black/mode.py | 20 +++++++++++++++++- tests/test_black.py | 7 ++++++- tests/test_format.py | 24 ++++++++-------------- 9 files changed, 58 insertions(+), 45 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4b9ceae81dc..c3e2a3350d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,8 @@ - Fix handling of standalone `match()` or `case()` when there is a trailing newline or a comment inside of the parentheses. (#2760) - Black now normalizes string prefix order (#2297) +- Deprecate `--experimental-string-processing` and move the functionality under + `--preview` (#2789) ### Packaging @@ -38,7 +40,8 @@ ### Preview style -- Introduce the `--preview` flag with no style changes (#2752) +- Introduce the `--preview` flag (#2752) +- Add `--experimental-string-processing` to the preview style (#2789) ### Integrations diff --git a/docs/faq.md b/docs/faq.md index c7d5ec33ad9..94a978d826f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -33,7 +33,7 @@ still proposed on the issue tracker. See Starting in 2022, the formatting output will be stable for the releases made in the same year (other than unintentional bugs). It is possible to opt-in to the latest formatting -styles, using the `--future` flag. +styles, using the `--preview` flag. ## Why is my file not formatted? diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 11fe2c8cceb..1d1e42e75c8 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -10,6 +10,10 @@ with `# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip [YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a courtesy for straddling code. +The rest of this document describes the current formatting style. If you're interested +in trying out where the style is heading, see [future style](./future_style.md) and try +running `black --preview`. + ### How _Black_ wraps lines _Black_ ignores previous formatting and applies uniform horizontal and vertical @@ -260,16 +264,6 @@ If you are adopting _Black_ in a large project with pre-existing string conventi you can pass `--skip-string-normalization` on the command line. This is meant as an adoption helper, avoid using this for new projects. -(labels/experimental-string)= - -As an experimental option (can be enabled by `--experimental-string-processing`), -_Black_ splits long strings (using parentheses where appropriate) and merges short ones. -When split, parts of f-strings that don't need formatting are converted to plain -strings. User-made splits are respected when they do not exceed the line length limit. -Line continuation backslashes are converted into parenthesized strings. Unnecessary -parentheses are stripped. Because the functionality is experimental, feedback and issue -reports are highly encouraged! - _Black_ also processes docstrings. Firstly the indentation of docstrings is corrected for both quotations and the text within, although relative indentation in the text is preserved. Superfluous trailing whitespace on each line and unnecessary new lines at the diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 70ffeefc76a..2ec2c0333a5 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -34,15 +34,18 @@ with \ Although when the target version is Python 3.9 or higher, _Black_ will use parentheses instead since they're allowed in Python 3.9 and higher. -## Improved string processing - -Currently, _Black_ does not split long strings to fit the line length limit. Currently, -there is [an experimental option](labels/experimental-string) to enable splitting -strings. We plan to enable this option by default once it is fully stable. This is -tracked in [this issue](https://github.com/psf/black/issues/2188). - ## Preview style Experimental, potentially disruptive style changes are gathered under the `--preview` CLI flag. At the end of each year, these changes may be adopted into the default style, -as described in [The Black Code Style](./index.rst). +as described in [The Black Code Style](./index.rst). Because the functionality is +experimental, feedback and issue reports are highly encouraged! + +### Improved string processing + +_Black_ will split long string literals and merge short ones. Parentheses are used where +appropriate. When split, parts of f-strings that don't need formatting are converted to +plain strings. User-made splits are respected when they do not exceed the line length +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). diff --git a/src/black/__init__.py b/src/black/__init__.py index 405a01082e7..67c272e3cc9 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -241,10 +241,7 @@ def validate_regex( "--experimental-string-processing", is_flag=True, hidden=True, - help=( - "Experimental option that performs more normalization on string literals." - " Currently disabled because it leads to some crashes." - ), + help="(DEPRECATED and now included in --preview) Normalize string literals.", ) @click.option( "--preview", diff --git a/src/black/linegen.py b/src/black/linegen.py index 6008c773f94..9ee42aaaf72 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -23,8 +23,7 @@ from black.strings import normalize_string_prefix, normalize_string_quotes from black.trans import Transformer, CannotTransform, StringMerger from black.trans import StringSplitter, StringParenWrapper, StringParenStripper -from black.mode import Mode -from black.mode import Feature +from black.mode import Mode, Feature, Preview from blib2to3.pytree import Node, Leaf from blib2to3.pgen2 import token @@ -338,7 +337,7 @@ def transform_line( and not (line.inside_brackets and line.contains_standalone_comments()) ): # Only apply basic string preprocessing, since lines shouldn't be split here. - if mode.experimental_string_processing: + if Preview.string_processing in mode: transformers = [string_merge, string_paren_strip] else: transformers = [] @@ -381,7 +380,7 @@ def _rhs( # via type ... https://github.com/mypyc/mypyc/issues/884 rhs = type("rhs", (), {"__call__": _rhs})() - if mode.experimental_string_processing: + if Preview.string_processing in mode: if line.inside_brackets: transformers = [ string_merge, diff --git a/src/black/mode.py b/src/black/mode.py index c8c2bd4eb26..b6d1a1fbbef 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -7,9 +7,10 @@ import sys from dataclasses import dataclass, field -from enum import Enum +from enum import Enum, auto from operator import attrgetter from typing import Dict, Set +from warnings import warn if sys.version_info < (3, 8): from typing_extensions import Final @@ -124,6 +125,13 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" + string_processing = auto() + hug_simple_powers = auto() + + +class Deprecated(UserWarning): + """Visible deprecation warning.""" + @dataclass class Mode: @@ -136,6 +144,14 @@ class Mode: experimental_string_processing: bool = False preview: bool = False + def __post_init__(self) -> None: + if self.experimental_string_processing: + warn( + "`experimental string processing` has been included in `preview`" + " and deprecated. Use `preview` instead.", + Deprecated, + ) + def __contains__(self, feature: Preview) -> bool: """ Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag. @@ -143,6 +159,8 @@ def __contains__(self, feature: Preview) -> bool: The argument is not checked and features are not differentiated. They only exist to make development easier by clarifying intent. """ + if feature is Preview.string_processing: + return self.preview or self.experimental_string_processing return self.preview def get_cache_key(self) -> str: diff --git a/tests/test_black.py b/tests/test_black.py index 202fe23ddcd..19cff23cb89 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -150,6 +150,11 @@ def test_empty_ff(self) -> None: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) + def test_experimental_string_processing_warns(self) -> None: + self.assertWarns( + black.mode.Deprecated, black.Mode, experimental_string_processing=True + ) + def test_piping(self) -> None: source, expected = read_data("src/black/__init__", data=False) result = BlackRunner().invoke( @@ -342,7 +347,7 @@ def test_detect_pos_only_arguments(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("string_quotes") - mode = black.Mode(experimental_string_processing=True) + mode = black.Mode(preview=True) assert_format(source, expected, mode) mode = replace(mode, string_normalization=False) not_normalized = fs(source, mode=mode) diff --git a/tests/test_format.py b/tests/test_format.py index 00cd07f36f7..40f225c9554 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -55,15 +55,6 @@ "tupleassign", ] -EXPERIMENTAL_STRING_PROCESSING_CASES: List[str] = [ - "cantfit", - "comments7", - "long_strings", - "long_strings__edge_case", - "long_strings__regression", - "percent_precedence", -] - PY310_CASES: List[str] = [ "pattern_matching_simple", "pattern_matching_complex", @@ -73,7 +64,15 @@ "parenthesized_context_managers", ] -PREVIEW_CASES: List[str] = [] +PREVIEW_CASES: List[str] = [ + # string processing + "cantfit", + "comments7", + "long_strings", + "long_strings__edge_case", + "long_strings__regression", + "percent_precedence", +] SOURCES: List[str] = [ "src/black/__init__.py", @@ -136,11 +135,6 @@ def test_simple_format(filename: str) -> None: check_file(filename, DEFAULT_MODE) -@pytest.mark.parametrize("filename", EXPERIMENTAL_STRING_PROCESSING_CASES) -def test_experimental_format(filename: str) -> None: - check_file(filename, black.Mode(experimental_string_processing=True)) - - @pytest.mark.parametrize("filename", PREVIEW_CASES) def test_preview_format(filename: str) -> None: check_file(filename, black.Mode(preview=True)) From e66e0f8ff046e532e8129c78894ca1c4095c5c8b Mon Sep 17 00:00:00 2001 From: emfdavid <84335963+emfdavid@users.noreply.github.com> Date: Thu, 20 Jan 2022 18:48:49 -0500 Subject: [PATCH 02/50] Hint at likely cause of ast parsing failure in error message (#2786) Co-authored-by: Batuhan Taskaya Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- src/black/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 67c272e3cc9..bdece687e45 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1312,7 +1312,10 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: src_ast = parse_ast(src) except Exception as exc: raise AssertionError( - f"cannot use --safe with this file; failed to parse source file: {exc}" + f"cannot use --safe with this file; failed to parse source file AST: " + f"{exc}\n" + f"This could be caused by running Black with an older Python version " + f"that does not support new syntax used in your source file." ) from exc try: From 4ea75cd49521ed7fd8384e7a739e1abb1b6de46a Mon Sep 17 00:00:00 2001 From: Michael Marino Date: Fri, 21 Jan 2022 01:45:28 +0100 Subject: [PATCH 03/50] Add support for custom python cell magics (#2744) Fixes #2742. This PR adds the ability to configure additional python cell magics. This will allow formatting cells in Jupyter Notebooks that are using custom (python) magics. --- CHANGES.md | 2 ++ src/black/__init__.py | 22 +++++++++++++-- src/black/mode.py | 3 ++ tests/test.toml | 1 + tests/test_black.py | 1 + tests/test_ipynb.py | 66 ++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 88 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c3e2a3350d3..a2e5c0a4ff8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,8 @@ - Fix handling of standalone `match()` or `case()` when there is a trailing newline or a comment inside of the parentheses. (#2760) - Black now normalizes string prefix order (#2297) +- Add configuration option (`python-cell-magics`) to format cells with custom magics in + Jupyter Notebooks (#2744) - Deprecate `--experimental-string-processing` and move the functionality under `--preview` (#2789) diff --git a/src/black/__init__.py b/src/black/__init__.py index bdece687e45..eaf72f9c2b3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -24,6 +24,7 @@ MutableMapping, Optional, Pattern, + Sequence, Set, Sized, Tuple, @@ -225,6 +226,16 @@ def validate_regex( "(useful when piping source on standard input)." ), ) +@click.option( + "--python-cell-magics", + multiple=True, + help=( + "When processing Jupyter Notebooks, add the given magic to the list" + f" of known python-magics ({', '.join(PYTHON_CELL_MAGICS)})." + " Useful for formatting cells with custom python magics." + ), + default=[], +) @click.option( "-S", "--skip-string-normalization", @@ -401,6 +412,7 @@ def main( fast: bool, pyi: bool, ipynb: bool, + python_cell_magics: Sequence[str], skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, @@ -476,6 +488,7 @@ def main( magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, preview=preview, + python_cell_magics=set(python_cell_magics), ) if code is not None: @@ -981,7 +994,7 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo return dst_contents -def validate_cell(src: str) -> None: +def validate_cell(src: str, mode: Mode) -> None: """Check that cell does not already contain TransformerManager transformations, or non-Python cell magics, which might cause tokenizer_rt to break because of indentations. @@ -1000,7 +1013,10 @@ def validate_cell(src: str) -> None: """ if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS): raise NothingChanged - if src[:2] == "%%" and src.split()[0][2:] not in PYTHON_CELL_MAGICS: + if ( + src[:2] == "%%" + and src.split()[0][2:] not in PYTHON_CELL_MAGICS | mode.python_cell_magics + ): raise NothingChanged @@ -1020,7 +1036,7 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str: could potentially be automagics or multi-line magics, which are currently not supported. """ - validate_cell(src) + validate_cell(src, mode) src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( src ) diff --git a/src/black/mode.py b/src/black/mode.py index b6d1a1fbbef..6d45e3dc4da 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -4,6 +4,7 @@ chosen by the user. """ +from hashlib import md5 import sys from dataclasses import dataclass, field @@ -142,6 +143,7 @@ class Mode: is_ipynb: bool = False magic_trailing_comma: bool = True experimental_string_processing: bool = False + python_cell_magics: Set[str] = field(default_factory=set) preview: bool = False def __post_init__(self) -> None: @@ -180,5 +182,6 @@ def get_cache_key(self) -> str: str(int(self.magic_trailing_comma)), str(int(self.experimental_string_processing)), str(int(self.preview)), + md5((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), ] return ".".join(parts) diff --git a/tests/test.toml b/tests/test.toml index d3ab1e61202..e5fb9228f19 100644 --- a/tests/test.toml +++ b/tests/test.toml @@ -7,6 +7,7 @@ line-length = 79 target-version = ["py36", "py37", "py38"] exclude='\.pyi?$' include='\.py?$' +python-cell-magics = ["custom1", "custom2"] [v1.0.0-syntax] # This shouldn't break Black. diff --git a/tests/test_black.py b/tests/test_black.py index 19cff23cb89..fd01425ae74 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1322,6 +1322,7 @@ def test_parse_pyproject_toml(self) -> None: self.assertEqual(config["color"], True) self.assertEqual(config["line_length"], 79) self.assertEqual(config["target_version"], ["py36", "py37", "py38"]) + self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"]) self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index fe8d67a7777..d78a68cd9a0 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -1,5 +1,8 @@ +from dataclasses import replace import pathlib import re +from contextlib import ExitStack as does_not_raise +from typing import ContextManager from click.testing import CliRunner from black.handle_ipynb_magics import jupyter_dependencies_are_installed @@ -63,9 +66,19 @@ def test_trailing_semicolon_noop() -> None: format_cell(src, fast=True, mode=JUPYTER_MODE) -def test_cell_magic() -> None: +@pytest.mark.parametrize( + "mode", + [ + pytest.param(JUPYTER_MODE, id="default mode"), + pytest.param( + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + id="custom cell magics mode", + ), + ], +) +def test_cell_magic(mode: Mode) -> None: src = "%%time\nfoo =bar" - result = format_cell(src, fast=True, mode=JUPYTER_MODE) + result = format_cell(src, fast=True, mode=mode) expected = "%%time\nfoo = bar" assert result == expected @@ -76,6 +89,16 @@ def test_cell_magic_noop() -> None: format_cell(src, fast=True, mode=JUPYTER_MODE) +@pytest.mark.parametrize( + "mode", + [ + pytest.param(JUPYTER_MODE, id="default mode"), + pytest.param( + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + id="custom cell magics mode", + ), + ], +) @pytest.mark.parametrize( "src, expected", ( @@ -96,8 +119,8 @@ def test_cell_magic_noop() -> None: pytest.param("env = %env", "env = %env", id="Assignment to magic"), ), ) -def test_magic(src: str, expected: str) -> None: - result = format_cell(src, fast=True, mode=JUPYTER_MODE) +def test_magic(src: str, expected: str, mode: Mode) -> None: + result = format_cell(src, fast=True, mode=mode) assert result == expected @@ -139,6 +162,41 @@ def test_cell_magic_with_magic() -> None: assert result == expected +@pytest.mark.parametrize( + "mode, expected_output, expectation", + [ + pytest.param( + JUPYTER_MODE, + "%%custom_python_magic -n1 -n2\nx=2", + pytest.raises(NothingChanged), + id="No change when cell magic not registered", + ), + pytest.param( + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + "%%custom_python_magic -n1 -n2\nx=2", + pytest.raises(NothingChanged), + id="No change when other cell magics registered", + ), + pytest.param( + replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}), + "%%custom_python_magic -n1 -n2\nx = 2", + does_not_raise(), + id="Correctly change when cell magic registered", + ), + ], +) +def test_cell_magic_with_custom_python_magic( + mode: Mode, expected_output: str, expectation: ContextManager[object] +) -> None: + with expectation: + result = format_cell( + "%%custom_python_magic -n1 -n2\nx=2", + fast=True, + mode=mode, + ) + assert result == expected_output + + def test_cell_magic_nested() -> None: src = "%%time\n%%time\n2+2" result = format_cell(src, fast=True, mode=JUPYTER_MODE) From e0c572833a3e2b42cd45237c26a67c6f5be4b09d Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 21 Jan 2022 22:24:57 +0530 Subject: [PATCH 04/50] Set `click` lower bound to `8.0.0` (#2791) Closes #2774 --- CHANGES.md | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a2e5c0a4ff8..83117e65dc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,7 @@ - All upper version bounds on dependencies have been removed (#2718) - `typing-extensions` is no longer a required dependency in Python 3.10+ (#2772) +- Set `click` lower bound to `8.0.0` (#2791) ### Preview style diff --git a/setup.py b/setup.py index c31baab00ae..c5917998da4 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def find_python_files(base: Path) -> List[Path]: python_requires=">=3.6.2", zip_safe=False, install_requires=[ - "click>=7.1.2", + "click>=8.0.0", "platformdirs>=2", "tomli>=1.1.0", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", From 95c03b9638e44eb76611a0e005d447472a4f2f97 Mon Sep 17 00:00:00 2001 From: Rob Hammond <13874373+RHammond2@users.noreply.github.com> Date: Fri, 21 Jan 2022 11:23:26 -0700 Subject: [PATCH 05/50] add wind technology software projects using black (#2792) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32db2bf2ce8..daeb7473583 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,8 @@ code in compliance with many other _Black_ formatted projects. The following notable open-source projects trust _Black_ with enforcing a consistent code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, -Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, and -many more. +Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, +OpenOA, FLORIS, ORBIT, WOMBAT, and many more. The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Mozilla, Quora, Duolingo, QuantumBlack, Tesla. From d24bc4364c6ef2337875be1a5b4e0851adaaf0f6 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:00:13 -0500 Subject: [PATCH 06/50] Switch to Furo (#2793) - Add Furo dependency to docs/requirements.txt - Drop a fair bit of theme configuration - Fix the toctree declarations in index.rst - Move stuff around as Furo isn't 100% compatible with Alabaster Furo was chosen as it provides excellent mobile support, user controllable light/dark theming, and is overall easier to read --- CHANGES.md | 2 ++ docs/_static/custom.css | 44 ----------------------------------------- docs/conf.py | 25 ++--------------------- docs/faq.md | 1 + docs/index.rst | 13 ++++++++---- docs/requirements.txt | 1 + 6 files changed, 15 insertions(+), 71 deletions(-) delete mode 100644 docs/_static/custom.css diff --git a/CHANGES.md b/CHANGES.md index 83117e65dc4..4f4c6a2ffc7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,8 @@ ### Documentation - Change protocol in pip installation instructions to `https://` (#2761) +- Change HTML theme to Furo primarily for its responsive design and mobile support + (#2793) ## 21.12b0 diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index eacd69c15a0..00000000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,44 +0,0 @@ -/* Make the sidebar scrollable. Fixes https://github.com/psf/black/issues/990 */ -div.sphinxsidebar { - max-height: calc(100% - 18px); - overflow-y: auto; -} - -/* Hide scrollbar for Chrome, Safari and Opera */ -div.sphinxsidebar::-webkit-scrollbar { - display: none; -} - -/* Hide scrollbar for IE 6, 7 and 8 */ -@media \0screen\, screen\9 { - div.sphinxsidebar { - -ms-overflow-style: none; - } -} - -/* Hide scrollbar for IE 9 and 10 */ -/* backslash-9 removes ie11+ & old Safari 4 */ -@media screen and (min-width: 0\0) { - div.sphinxsidebar { - -ms-overflow-style: none\9; - } -} - -/* Hide scrollbar for IE 11 and up */ -_:-ms-fullscreen, -:root div.sphinxsidebar { - -ms-overflow-style: none; -} - -/* Hide scrollbar for Edge */ -@supports (-ms-ime-align: auto) { - div.sphinxsidebar { - -ms-overflow-style: none; - } -} - -/* Nicer style for local document toc */ -.contents.topic { - background: none; - border: none; -} diff --git a/docs/conf.py b/docs/conf.py index 55d0fa99dc6..2801e0eed19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -115,29 +115,8 @@ def make_pypi_svg(version: str) -> None: # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "alabaster" - -html_sidebars = { - "**": [ - "about.html", - "navigation.html", - "relations.html", - "searchbox.html", - ] -} - -html_theme_options = { - "show_related": False, - "description": "“Any color you like.”", - "github_button": True, - "github_user": "psf", - "github_repo": "black", - "github_type": "star", - "show_powered_by": True, - "fixed_sidebar": True, - "logo": "logo2.png", -} - +html_theme = "furo" +html_logo = "_static/logo2-readme.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/faq.md b/docs/faq.md index 94a978d826f..1ebdcd9530d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,6 +5,7 @@ The most common questions and issues users face are aggregated to this FAQ. ```{contents} :local: :backlinks: none +:class: this-will-duplicate-information-and-it-is-still-useful-here ``` ## Does Black have an API? diff --git a/docs/index.rst b/docs/index.rst index 1515697f556..6818c03cfe9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,8 @@ The uncompromising code formatter ================================= + “Any color you like.” + By using *Black*, you agree to cede control over minutiae of hand-formatting. In return, *Black* gives you speed, determinism, and freedom from `pycodestyle` nagging about formatting. You will save time @@ -99,6 +101,7 @@ Contents .. toctree:: :maxdepth: 3 :includehidden: + :caption: User Guide getting_started usage_and_configuration/index @@ -107,8 +110,9 @@ Contents faq .. toctree:: - :maxdepth: 3 + :maxdepth: 2 :includehidden: + :caption: Development contributing/index change_log @@ -116,10 +120,11 @@ Contents .. toctree:: :hidden: + :caption: Project Links - GitHub ↪ - PyPI ↪ - Chat ↪ + GitHub + PyPI + Chat Indices and tables ================== diff --git a/docs/requirements.txt b/docs/requirements.txt index 02874d3c255..01fea693f07 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,3 +4,4 @@ myst-parser==0.16.1 Sphinx==4.4.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 +furo==2022.1.2 From 10677baa40f818ca06c6a9d5efa0dca052865bfb Mon Sep 17 00:00:00 2001 From: Perry Vargas Date: Fri, 21 Jan 2022 22:00:33 -0800 Subject: [PATCH 07/50] Allow setting custom cache directory on all platforms (#2739) Fixes #2506 ``XDG_CACHE_HOME`` does not work on Windows. To allow for users to set a custom cache directory on all systems I added a new environment variable ``BLACK_CACHE_DIR`` to set the cache directory. The default remains the same so users will only notice a change if that environment variable is set. The specific use case I have for this is I need to run black on in different processes at the same time. There is a race condition with the cache pickle file that made this rather difficult. A custom cache directory will remove the race condition. I created ``get_cache_dir`` function in order to test the logic. This is only used to set the ``CACHE_DIR`` constant. --- CHANGES.md | 2 ++ .../reference/reference_functions.rst | 2 ++ .../file_collection_and_discovery.md | 10 ++++--- src/black/cache.py | 18 +++++++++++- tests/test_black.py | 29 ++++++++++++++++++- 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4f4c6a2ffc7..dc52ca34cbb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ - Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700) - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) +- Allow setting custom cache directory on all platforms with environment variable + `BLACK_CACHE_DIR` (#2739). - Text coloring added in the final statistics (#2712) - For stubs, one blank line between class attributes and methods is now kept if there's at least one pre-existing blank line (#2736) diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index 4353d1bf9a9..01ffe44ef53 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -96,6 +96,8 @@ Caching .. autofunction:: black.cache.filter_cached +.. autofunction:: black.cache.get_cache_dir + .. autofunction:: black.cache.get_cache_file .. autofunction:: black.cache.get_cache_info diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md index 1f436182dda..bd90ccc6af8 100644 --- a/docs/usage_and_configuration/file_collection_and_discovery.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -22,10 +22,12 @@ run. The file is non-portable. The standard location on common operating systems `file-mode` is an int flag that determines whether the file was formatted as 3.6+ only, as .pyi, and whether string normalization was omitted. -To override the location of these files on macOS or Linux, set the environment variable -`XDG_CACHE_HOME` to your preferred location. For example, if you want to put the cache -in the directory you're running _Black_ from, set `XDG_CACHE_HOME=.cache`. _Black_ will -then write the above files to `.cache/black//`. +To override the location of these files on all systems, set the environment variable +`BLACK_CACHE_DIR` to the preferred location. Alternatively on macOS and Linux, set +`XDG_CACHE_HOME` to you your preferred location. For example, if you want to put the +cache in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`. +_Black_ will then write the above files to `.cache/black`. Note that `BLACK_CACHE_DIR` +will take precedence over `XDG_CACHE_HOME` if both are set. ## .gitignore diff --git a/src/black/cache.py b/src/black/cache.py index bca7279f990..552c248d2ad 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -20,7 +20,23 @@ Cache = Dict[str, CacheInfo] -CACHE_DIR = Path(user_cache_dir("black", version=__version__)) +def get_cache_dir() -> Path: + """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 + under the black application. + + This result is immediately set to a constant `black.cache.CACHE_DIR` as to avoid + repeated calls. + """ + # NOTE: Function mostly exists as a clean way to test getting the cache directory. + default_cache_dir = user_cache_dir("black", version=__version__) + cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir)) + return cache_dir + + +CACHE_DIR = get_cache_dir() def read_cache(mode: Mode) -> Cache: diff --git a/tests/test_black.py b/tests/test_black.py index fd01425ae74..559690938a8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -40,7 +40,7 @@ import black.files from black import Feature, TargetVersion from black import re_compile_maybe_verbose as compile_pattern -from black.cache import get_cache_file +from black.cache import get_cache_dir, get_cache_file from black.debug import DebugVisitor from black.output import color_diff, diff from black.report import Report @@ -1601,6 +1601,33 @@ def test_equivalency_ast_parse_failure_includes_error(self) -> None: class TestCaching: + def test_get_cache_dir( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + # Create multiple cache directories + workspace1 = tmp_path / "ws1" + workspace1.mkdir() + workspace2 = tmp_path / "ws2" + workspace2.mkdir() + + # Force user_cache_dir to use the temporary directory for easier assertions + patch_user_cache_dir = patch( + target="black.cache.user_cache_dir", + autospec=True, + return_value=str(workspace1), + ) + + # If BLACK_CACHE_DIR is not set, use user_cache_dir + monkeypatch.delenv("BLACK_CACHE_DIR", raising=False) + with patch_user_cache_dir: + assert get_cache_dir() == workspace1 + + # If it is set, use the path provided in the env var. + monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2)) + assert get_cache_dir() == workspace2 + def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: From fb1d1b2fc85efe422b6ff32d05f537d5394f6259 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 22 Jan 2022 06:08:27 -0500 Subject: [PATCH 08/50] Mark Felix and Batuhan as maintainers (#2794) Y'all deserve it :) --- AUTHORS.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 8d112ea6795..8aa6263313e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -2,12 +2,17 @@ Glued together by [Łukasz Langa](mailto:lukasz@langa.pl). -Maintained with [Carol Willing](mailto:carolcode@willingconsulting.com), -[Carl Meyer](mailto:carl@oddbird.net), -[Jelle Zijlstra](mailto:jelle.zijlstra@gmail.com), -[Mika Naylor](mailto:mail@autophagy.io), -[Zsolt Dollenstein](mailto:zsol.zsol@gmail.com), -[Cooper Lees](mailto:me@cooperlees.com), and Richard Si. +Maintained with: + +- [Carol Willing](mailto:carolcode@willingconsulting.com) +- [Carl Meyer](mailto:carl@oddbird.net) +- [Jelle Zijlstra](mailto:jelle.zijlstra@gmail.com) +- [Mika Naylor](mailto:mail@autophagy.io) +- [Zsolt Dollenstein](mailto:zsol.zsol@gmail.com) +- [Cooper Lees](mailto:me@cooperlees.com) +- Richard Si +- [Felix Hildén](mailto:felix.hilden@gmail.com) +- [Batuhan Taskaya](mailto:batuhan@python.org) Multiple contributions by: From 811de5f36bb1bb2bc7e14c186cf1af6badb77475 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 22 Jan 2022 07:29:38 -0800 Subject: [PATCH 09/50] Refactor logic for stub empty lines (#2796) This PR is intended to have no change to semantics. This is in preparation for #2784 which will likely introduce more logic that depends on `current_line.depth`. Inlining the subtraction gets rid of offsetting and makes it much easier to see what the result will be. --- src/black/lines.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index d8617d83bf7..c602aa69ce9 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -529,9 +529,11 @@ def _maybe_empty_lines_for_class_or_def( if self.is_pyi: if self.previous_line.depth > current_line.depth: - newlines = 1 + newlines = 0 if current_line.depth else 1 elif current_line.is_class or self.previous_line.is_class: - if current_line.is_stub_class and self.previous_line.is_stub_class: + if current_line.depth: + newlines = 0 + elif current_line.is_stub_class and self.previous_line.is_stub_class: # No blank line between classes with an empty body newlines = 0 else: @@ -539,21 +541,18 @@ def _maybe_empty_lines_for_class_or_def( elif ( current_line.is_def or current_line.is_decorator ) and not self.previous_line.is_def: - if not current_line.depth: + if current_line.depth: + # In classes empty lines between attributes and methods should + # be preserved. + newlines = min(1, before) + else: # Blank line between a block of functions (maybe with preceding # decorators) and a block of non-functions newlines = 1 - else: - # In classes empty lines between attributes and methods should - # be preserved. The +1 offset is to negate the -1 done later as - # this function is indented. - newlines = min(2, before + 1) else: newlines = 0 else: - newlines = 2 - if current_line.depth and newlines: - newlines -= 1 + newlines = 1 if current_line.depth else 2 return newlines, 0 From b3b341b44fde044938daa6691fa1064ea240ff96 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 22 Jan 2022 07:30:18 -0800 Subject: [PATCH 10/50] Mention "skip news" label in CHANGELOG action (#2797) Co-authored-by: hauntsaninja <> --- .github/workflows/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index d7ee50558d3..476e2545ce8 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -17,5 +17,5 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'skip news') != true run: | grep -Pz "\((\n\s*)?#${{ github.event.pull_request.number }}(\n\s*)?\)" CHANGES.md || \ - (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md" && \ + (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md (or if appropriate, ask a maintainer to add the 'skip news' label)" && \ exit 1) From 022f89625f9bb33ab55c82c45ec0eb8512623fd3 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sat, 22 Jan 2022 23:05:26 +0300 Subject: [PATCH 11/50] Enable pattern matching by default (#2758) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 2 ++ src/black/parsing.py | 28 ++++++++++++++++------------ src/blib2to3/pgen2/grammar.py | 2 ++ src/blib2to3/pygram.py | 5 +++++ tests/test_format.py | 15 ++++++--------- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dc52ca34cbb..634db79bf73 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,8 @@ Jupyter Notebooks (#2744) - Deprecate `--experimental-string-processing` and move the functionality under `--preview` (#2789) +- Enable Python 3.10+ by default, without any extra need to specify + `--target-version=py310`. (#2758) ### Packaging diff --git a/src/black/parsing.py b/src/black/parsing.py index 6b63368871c..db48ae4baf5 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -42,7 +42,6 @@ ast3 = ast -PY310_HINT: Final = "Consider using --target-version py310 to parse Python 3.10 code." PY2_HINT: Final = "Python 2 support was removed in version 22.0." @@ -58,12 +57,11 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, # Python 3.0-3.6 pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 3.10+ + pygram.python_grammar_soft_keywords, ] grammars = [] - if supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.10+ - grammars.append(pygram.python_grammar_soft_keywords) # If we have to parse both, try to parse async as a keyword first if not supports_feature( target_versions, Feature.ASYNC_IDENTIFIERS @@ -75,6 +73,10 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): # Python 3.0-3.6 grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # At least one of the above branches must have been taken, because every Python # version has exactly one of the two 'ASYNC_*' flags return grammars @@ -86,6 +88,7 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - src_txt += "\n" grammars = get_grammars(set(target_versions)) + errors = {} for grammar in grammars: drv = driver.Driver(grammar) try: @@ -99,20 +102,21 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - faulty_line = lines[lineno - 1] except IndexError: faulty_line = "" - exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}") + errors[grammar.version] = InvalidInput( + f"Cannot parse: {lineno}:{column}: {faulty_line}" + ) except TokenError as te: # In edge cases these are raised; and typically don't have a "faulty_line". lineno, column = te.args[1] - exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {te.args[0]}") + errors[grammar.version] = InvalidInput( + f"Cannot parse: {lineno}:{column}: {te.args[0]}" + ) else: - if pygram.python_grammar_soft_keywords not in grammars and matches_grammar( - src_txt, pygram.python_grammar_soft_keywords - ): - original_msg = exc.args[0] - msg = f"{original_msg}\n{PY310_HINT}" - raise InvalidInput(msg) from None + # Choose the latest version when raising the actual parsing error. + assert len(errors) >= 1 + exc = errors[max(errors)] if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar( src_txt, pygram.python_grammar_no_print_statement diff --git a/src/blib2to3/pgen2/grammar.py b/src/blib2to3/pgen2/grammar.py index 56851070933..337a64f1726 100644 --- a/src/blib2to3/pgen2/grammar.py +++ b/src/blib2to3/pgen2/grammar.py @@ -92,6 +92,7 @@ def __init__(self) -> None: self.soft_keywords: Dict[str, int] = {} self.tokens: Dict[int, int] = {} self.symbol2label: Dict[str, int] = {} + self.version: Tuple[int, int] = (0, 0) self.start = 256 # Python 3.7+ parses async as a keyword, not an identifier self.async_keywords = False @@ -145,6 +146,7 @@ def copy(self: _P) -> _P: new.labels = self.labels[:] new.states = self.states[:] new.start = self.start + new.version = self.version new.async_keywords = self.async_keywords return new diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index aa20b8104ae..a3df9be1265 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -178,6 +178,8 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: # Python 2 python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir) + python_grammar.version = (2, 0) + soft_keywords = python_grammar.soft_keywords.copy() python_grammar.soft_keywords.clear() @@ -191,6 +193,7 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: python_grammar_no_print_statement_no_exec_statement = python_grammar.copy() del python_grammar_no_print_statement_no_exec_statement.keywords["print"] del python_grammar_no_print_statement_no_exec_statement.keywords["exec"] + python_grammar_no_print_statement_no_exec_statement.version = (3, 0) # Python 3.7+ python_grammar_no_print_statement_no_exec_statement_async_keywords = ( @@ -199,12 +202,14 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: python_grammar_no_print_statement_no_exec_statement_async_keywords.async_keywords = ( True ) + python_grammar_no_print_statement_no_exec_statement_async_keywords.version = (3, 7) # Python 3.10+ python_grammar_soft_keywords = ( python_grammar_no_print_statement_no_exec_statement_async_keywords.copy() ) python_grammar_soft_keywords.soft_keywords = soft_keywords + python_grammar_soft_keywords.version = (3, 10) pattern_grammar = driver.load_packaged_grammar( "blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir diff --git a/tests/test_format.py b/tests/test_format.py index 40f225c9554..3895a095e86 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -191,6 +191,12 @@ def test_python_310(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 10)) +def test_python_310_without_target_version() -> None: + source, expected = read_data("pattern_matching_simple") + mode = black.Mode() + assert_format(source, expected, mode, minimum_version=(3, 10)) + + def test_patma_invalid() -> None: source, expected = read_data("pattern_matching_invalid") mode = black.Mode(target_versions={black.TargetVersion.PY310}) @@ -200,15 +206,6 @@ def test_patma_invalid() -> None: exc_info.match("Cannot parse: 10:11") -def test_patma_hint() -> None: - source, expected = read_data("pattern_matching_simple") - mode = black.Mode(target_versions={black.TargetVersion.PY39}) - with pytest.raises(black.parsing.InvalidInput) as exc_info: - assert_format(source, expected, mode, minimum_version=(3, 10)) - - exc_info.match(black.parsing.PY310_HINT) - - def test_python_2_hint() -> None: with pytest.raises(black.parsing.InvalidInput) as exc_info: assert_format("print 'daylily'", "print 'daylily'") From 6e3677f3f0c0542f858f7fc06d20cca5fab59348 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 23 Jan 2022 11:49:11 -0500 Subject: [PATCH 12/50] Allow blackd to be run as a package (#2800) --- src/blackd/__main__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/blackd/__main__.py diff --git a/src/blackd/__main__.py b/src/blackd/__main__.py new file mode 100644 index 00000000000..b5a4b137446 --- /dev/null +++ b/src/blackd/__main__.py @@ -0,0 +1,3 @@ +import blackd + +blackd.patched_main() From d2c938eb02c414057aa2186c7ae695d5d0d14377 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 23 Jan 2022 12:34:01 -0800 Subject: [PATCH 13/50] Remove Beta mentions in README + Docs (#2801) - State we're now stable and that we'll uphold our formatting changes as per policy - Link to The Black Style doc. Co-authored-by: Jelle Zijlstra --- README.md | 15 ++++++--------- docs/faq.md | 11 ++++------- docs/index.rst | 14 ++++++-------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index daeb7473583..e900d2d75a2 100644 --- a/README.md +++ b/README.md @@ -64,16 +64,13 @@ Further information can be found in our docs: - [Usage and Configuration](https://black.readthedocs.io/en/stable/usage_and_configuration/index.html) -### NOTE: This is a beta product - _Black_ is already [successfully used](https://github.com/psf/black#used-by) by many -projects, small and big. Black has a comprehensive test suite, with efficient parallel -tests, and our own auto formatting and parallel Continuous Integration runner. However, -_Black_ is still beta. Things will probably be wonky for a while. This is made explicit -by the "Beta" trove classifier, as well as by the "b" in the version number. What this -means for you is that **until the formatter becomes stable, you should expect some -formatting to change in the future**. That being said, no drastic stylistic changes are -planned, mostly responses to bug reports. +projects, small and big. _Black_ has a comprehensive test suite, with efficient parallel +tests, and our own auto formatting and parallel Continuous Integration runner. Now that +we have become stable, you should not expect large formatting to changes in the future. +Stylistic changes will mostly be responses to bug reports and support for new Python +syntax. For more information please refer to the +[The Black Code Style](docs/the_black_code_style/index.rst). Also, as a safety measure which slows down processing, _Black_ will check that the reformatted code still produces a valid AST that is effectively equivalent to the diff --git a/docs/faq.md b/docs/faq.md index 1ebdcd9530d..0cff6ae5e1d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -17,9 +17,8 @@ though. ## Is Black safe to use? -Yes, for the most part. _Black_ is strictly about formatting, nothing else. But because -_Black_ is still in [beta](index.rst), some edges are still a bit rough. To combat -issues, the equivalence of code after formatting is +Yes. _Black_ is strictly about formatting, nothing else. Black strives to ensure that +after formatting the AST is [checked](the_black_code_style/current_style.md#ast-before-and-after-formatting) with limited special cases where the code is allowed to differ. If issues are found, an error is raised and the file is left untouched. Magical comments that influence linters and @@ -27,10 +26,8 @@ other tools, such as `# noqa`, may be moved by _Black_. See below for more detai ## How stable is Black's style? -Quite stable. _Black_ aims to enforce one style and one style only, with some room for -pragmatism. However, _Black_ is still in beta so style changes are both planned and -still proposed on the issue tracker. See -[The Black Code Style](the_black_code_style/index.rst) for more details. +Stable. _Black_ aims to enforce one style and one style only, with some room for +pragmatism. See [The Black Code Style](the_black_code_style/index.rst) for more details. Starting in 2022, the formatting output will be stable for the releases made in the same year (other than unintentional bugs). It is possible to opt-in to the latest formatting diff --git a/docs/index.rst b/docs/index.rst index 6818c03cfe9..8a8da0d6127 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,16 +18,14 @@ can focus on the content instead. Try it out now using the `Black Playground `_. -.. admonition:: Note - this is a beta product +.. admonition:: Note - Black is now stable! - *Black* is already `successfully used `_ by + *Black* is `successfully used `_ by many projects, small and big. *Black* has a comprehensive test suite, with efficient - parallel tests, our own auto formatting and parallel Continuous Integration runner. - However, *Black* is still beta. Things will probably be wonky for a while. This is - made explicit by the "Beta" trove classifier, as well as by the "b" in the version - number. What this means for you is that **until the formatter becomes stable, you - should expect some formatting to change in the future**. That being said, no drastic - stylistic changes are planned, mostly responses to bug reports. + parallel tests, our own auto formatting and parallel Continuous Integration runner. + Now that we have become stable, you should not expect large formatting to changes in + the future. Stylistic changes will mostly be responses to bug reports and support for new Python + syntax. Also, as a safety measure which slows down processing, *Black* will check that the reformatted code still produces a valid AST that is effectively equivalent to the From 3905173cb32922b580bad184e724586f359c8c7e Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 23 Jan 2022 23:34:29 +0300 Subject: [PATCH 14/50] Use `magic_trailing_comma` and `preview` for `FileMode` in `fuzz` (#2802) Co-authored-by: Jelle Zijlstra --- fuzz.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuzz.py b/fuzz.py index a9ca8eff8b0..09a86a2f571 100644 --- a/fuzz.py +++ b/fuzz.py @@ -32,7 +32,9 @@ black.FileMode, line_length=st.just(88) | st.integers(0, 200), string_normalization=st.booleans(), + preview=st.booleans(), is_pyi=st.booleans(), + magic_trailing_comma=st.booleans(), ), ) def test_idempotent_any_syntatically_valid_python( From 73cb6e7734370108742d992d4fe1fa2829f100fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Mon, 24 Jan 2022 17:35:56 +0200 Subject: [PATCH 15/50] Make SRC or code mandatory and mutually exclusive (#2360) (#2804) Closes #2360: I'd like to make passing SRC or `--code` mandatory and the arguments mutually exclusive. This will change our (partially already broken) promises of CLI behavior, but I'll comment below. --- CHANGES.md | 1 + docs/usage_and_configuration/the_basics.md | 4 ++-- src/black/__init__.py | 12 +++++++++++- tests/test_black.py | 20 ++++++++++++++------ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 634db79bf73..458d48cd2c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,7 @@ `--preview` (#2789) - Enable Python 3.10+ by default, without any extra need to specify `--target-version=py310`. (#2758) +- Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804) ### Packaging diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index fd39b6c8979..b82cef4a52d 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -4,11 +4,11 @@ Foundational knowledge on using and configuring Black. _Black_ is a well-behaved Unix-style command-line tool: -- it does nothing if no sources are passed to it; +- it does nothing if it finds no sources to format; - it will read from standard input and write to standard output if `-` is used as the filename; - it only outputs messages to users on standard error; -- exits with code 0 unless an internal error occurred (or `--check` was used). +- exits with code 0 unless an internal error occurred or a CLI option prompted it. ## Usage diff --git a/src/black/__init__.py b/src/black/__init__.py index eaf72f9c2b3..7024c9d52b0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -431,6 +431,17 @@ def main( ) -> None: """The uncompromising code formatter.""" ctx.ensure_object(dict) + + if src and code is not None: + out( + main.get_usage(ctx) + + "\n\n'SRC' and 'code' cannot be passed simultaneously." + ) + ctx.exit(1) + if not src and code is None: + out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") + ctx.exit(1) + root, method = find_project_root(src) if code is None else (None, None) ctx.obj["root"] = root @@ -569,7 +580,6 @@ def get_sources( ) -> Set[Path]: """Compute the set of files to be formatted.""" sources: Set[Path] = set() - path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) if exclude is None: exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) diff --git a/tests/test_black.py b/tests/test_black.py index 559690938a8..8d691d2f019 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -972,10 +972,13 @@ def test_check_diff_use_together(self) -> None: # Multi file command. self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) - def test_no_files(self) -> None: + def test_no_src_fails(self) -> None: with cache_dir(): - # Without an argument, black exits with error code 0. - self.invokeBlack([]) + self.invokeBlack([], exit_code=1) + + def test_src_and_code_fails(self) -> None: + with cache_dir(): + self.invokeBlack([".", "-c", "0"], exit_code=1) def test_broken_symlink(self) -> None: with cache_dir() as workspace: @@ -1229,13 +1232,18 @@ def test_invalid_cli_regex(self) -> None: def test_required_version_matches_version(self) -> None: self.invokeBlack( - ["--required-version", black.__version__], exit_code=0, ignore_config=True + ["--required-version", black.__version__, "-c", "0"], + exit_code=0, + ignore_config=True, ) def test_required_version_does_not_match_version(self) -> None: - self.invokeBlack( - ["--required-version", "20.99b"], exit_code=1, ignore_config=True + result = BlackRunner().invoke( + black.main, + ["--required-version", "20.99b", "-c", "0"], ) + self.assertEqual(result.exit_code, 1) + self.assertIn("required version", result.stderr) def test_preserves_line_endings(self) -> None: with TemporaryDirectory() as workspace: From 6417c99bfdbdc057e4a10aeff9967a751f4f85e9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 24 Jan 2022 22:13:34 -0500 Subject: [PATCH 16/50] Hug power operators if its operands are "simple" (#2726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since power operators almost always have the highest binding power in expressions, it's often more readable to hug it with its operands. The main exception to this is when its operands are non-trivial in which case the power operator will not hug, the rule for this is the following: > For power ops, an operand is considered "simple" if it's only a NAME, numeric CONSTANT, or attribute access (chained attribute access is allowed), with or without a preceding unary operator. Fixes GH-538. Closes GH-2095. diff-shades results: https://gist.github.com/ichard26/ca6c6ad4bd1de5152d95418c8645354b Co-authored-by: Diego Co-authored-by: Felix Hildén Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + docs/the_black_code_style/current_style.md | 20 ++++ src/black/linegen.py | 7 +- src/black/trans.py | 86 ++++++++++++++- src/black_primer/primer.json | 2 +- tests/data/expression.diff | 44 +++++--- tests/data/expression.py | 30 ++--- .../expression_skip_magic_trailing_comma.diff | 44 +++++--- tests/data/pep_572.py | 2 +- tests/data/pep_572_py39.py | 2 +- tests/data/power_op_spacing.py | 103 ++++++++++++++++++ tests/data/slices.py | 2 +- tests/test_format.py | 1 + 13 files changed, 293 insertions(+), 51 deletions(-) create mode 100644 tests/data/power_op_spacing.py diff --git a/CHANGES.md b/CHANGES.md index 458d48cd2c1..d203896a801 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ - Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700) - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) +- Remove spaces around power operators if both operands are simple (#2726) - Allow setting custom cache directory on all platforms with environment variable `BLACK_CACHE_DIR` (#2739). - Text coloring added in the final statistics (#2712) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 1d1e42e75c8..5be7ba6dbdb 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -284,6 +284,26 @@ multiple lines. This is so that _Black_ is compliant with the recent changes in [PEP 8](https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator) style guide, which emphasizes that this approach improves readability. +Almost all operators will be surrounded by single spaces, the only exceptions are unary +operators (`+`, `-`, and `~`), and power operators when both operands are simple. For +powers, an operand is considered simple if it's only a NAME, numeric CONSTANT, or +attribute access (chained attribute access is allowed), with or without a preceding +unary operator. + +```python +# For example, these won't be surrounded by whitespace +a = x**y +b = config.base**5.2 +c = config.base**runtime.config.exponent +d = 2**5 +e = 2**~5 + +# ... but these will be surrounded by whitespace +f = 2 ** get_exponent() +g = get_x() ** get_y() +h = config['base'] ** 2 +``` + ### Slices PEP 8 diff --git a/src/black/linegen.py b/src/black/linegen.py index 9ee42aaaf72..9fbdfadba6a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -21,8 +21,8 @@ from black.numerics import normalize_numeric_literal from black.strings import get_string_prefix, fix_docstring from black.strings import normalize_string_prefix, normalize_string_quotes -from black.trans import Transformer, CannotTransform, StringMerger -from black.trans import StringSplitter, StringParenWrapper, StringParenStripper +from black.trans import Transformer, CannotTransform, StringMerger, StringSplitter +from black.trans import StringParenWrapper, StringParenStripper, hug_power_op from black.mode import Mode, Feature, Preview from blib2to3.pytree import Node, Leaf @@ -404,6 +404,9 @@ def _rhs( transformers = [delimiter_split, standalone_comment_split, rhs] else: transformers = [rhs] + # It's always safe to attempt hugging of power operations and pretty much every line + # could match. + transformers.append(hug_power_op) for transform in transformers: # We are accumulating lines in `result` because we might want to abort diff --git a/src/black/trans.py b/src/black/trans.py index cb41c1be487..74d052fe2dc 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -24,9 +24,9 @@ import sys if sys.version_info < (3, 8): - from typing_extensions import Final + from typing_extensions import Literal, Final else: - from typing import Final + from typing import Literal, Final from mypy_extensions import trait @@ -71,6 +71,88 @@ def TErr(err_msg: str) -> Err[CannotTransform]: return Err(cant_transform) +def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: + """A transformer which normalizes spacing around power operators.""" + + # Performance optimization to avoid unnecessary Leaf clones and other ops. + for leaf in line.leaves: + if leaf.type == token.DOUBLESTAR: + break + else: + raise CannotTransform("No doublestar token was found in the line.") + + def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool: + # Brackets and parentheses indicate calls, subscripts, etc. ... + # basically stuff that doesn't count as "simple". Only a NAME lookup + # or dotted lookup (eg. NAME.NAME) is OK. + if step == -1: + disallowed = {token.RPAR, token.RSQB} + else: + disallowed = {token.LPAR, token.LSQB} + + while 0 <= index < len(line.leaves): + current = line.leaves[index] + if current.type in disallowed: + return False + if current.type not in {token.NAME, token.DOT} or current.value == "for": + # If the current token isn't disallowed, we'll assume this is simple as + # only the disallowed tokens are semantically attached to this lookup + # expression we're checking. Also, stop early if we hit the 'for' bit + # of a comprehension. + return True + + index += step + + return True + + def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool: + # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple + # lookup (see above), with or without a preceding unary operator. + start = line.leaves[index] + if start.type in {token.NAME, token.NUMBER}: + return is_simple_lookup(index, step=(1 if kind == "exponent" else -1)) + + if start.type in {token.PLUS, token.MINUS, token.TILDE}: + if line.leaves[index + 1].type in {token.NAME, token.NUMBER}: + # step is always one as bases with a preceding unary op will be checked + # for simplicity starting from the next token (so it'll hit the check + # above). + return is_simple_lookup(index + 1, step=1) + + return False + + leaves: List[Leaf] = [] + should_hug = False + for idx, leaf in enumerate(line.leaves): + new_leaf = leaf.clone() + if should_hug: + new_leaf.prefix = "" + should_hug = False + + should_hug = ( + (0 < idx < len(line.leaves) - 1) + and leaf.type == token.DOUBLESTAR + and is_simple_operand(idx - 1, kind="base") + and line.leaves[idx - 1].value != "lambda" + and is_simple_operand(idx + 1, kind="exponent") + ) + if should_hug: + new_leaf.prefix = "" + + leaves.append(new_leaf) + + yield Line( + mode=line.mode, + depth=line.depth, + leaves=leaves, + comments=line.comments, + bracket_tracker=line.bracket_tracker, + inside_brackets=line.inside_brackets, + should_split_rhs=line.should_split_rhs, + magic_trailing_comma=line.magic_trailing_comma, + ) + + class StringTransformer(ABC): """ An implementation of the Transformer protocol that relies on its diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index a8d8fc9e21f..a6bfd4a2fec 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -81,7 +81,7 @@ }, "flake8-bugbear": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", "long_checkout": false, "py_versions": ["all"] diff --git a/tests/data/expression.diff b/tests/data/expression.diff index 721a07d2141..5f29a18dc7f 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -11,7 +11,17 @@ True False 1 -@@ -29,63 +29,96 @@ +@@ -21,71 +21,104 @@ + Name1 or (Name2 and Name3) or Name4 + Name1 or Name2 and Name3 or Name4 + v1 << 2 + 1 >> v2 + 1 % finished +-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 +-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) ++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) + not great ~great +value -1 @@ -19,7 +29,7 @@ (~int) and (not ((v1 ^ (123 + v2)) | True)) -+really ** -confusing ** ~operator ** -precedence -flags & ~ select.EPOLLIN and waiters.write_task is not None -++(really ** -(confusing ** ~(operator ** -precedence))) +++(really ** -(confusing ** ~(operator**-precedence))) +flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -88,15 +98,19 @@ + *more, +] {i for i in (1, 2, 3)} - {(i ** 2) for i in (1, 2, 3)} +-{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))} -+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} - {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} ++{(i**2) for i in (1, 2, 3)} ++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} ++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] - [(i ** 2) for i in (1, 2, 3)] +-[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))] -+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] - [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] ++[(i**2) for i in (1, 2, 3)] ++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] ++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))} +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} @@ -181,10 +195,12 @@ SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) - ((i ** 2) for i in (1, 2, 3)) +-((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))) -+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) - (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) ++((i**2) for i in (1, 2, 3)) ++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) ++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs} +{ @@ -403,13 +419,13 @@ + return True +if ( + ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -+ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n ++ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n +): + return True +if ( + ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -+ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n ++ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n +): + return True +if ( @@ -419,7 +435,7 @@ + | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k -+ >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ++ >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +): + return True +( diff --git a/tests/data/expression.py b/tests/data/expression.py index d13450cda68..b056841027d 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -282,15 +282,15 @@ async def f(): v1 << 2 1 >> v2 1 % finished -1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 -((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) +1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 +((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) not great ~great +value -1 ~int and not v1 ^ 123 + v2 | True (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator ** -precedence))) ++(really ** -(confusing ** ~(operator**-precedence))) flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -347,13 +347,13 @@ async def f(): *more, ] {i for i in (1, 2, 3)} -{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} -{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +{(i**2) for i in (1, 2, 3)} +{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} +{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] -[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] -[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +[(i**2) for i in (1, 2, 3)] +[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] +[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} {i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} {a: b * 2 for a, b in dictionary.items()} @@ -441,9 +441,9 @@ async def f(): SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) -((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) -(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +((i**2) for i in (1, 2, 3)) +((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) +(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) { "id": "1", @@ -588,13 +588,13 @@ async def f(): return True if ( ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e - | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h - ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True if ( @@ -604,7 +604,7 @@ async def f(): | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k - >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True ( diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff index 4a8a95c7237..5b722c91352 100644 --- a/tests/data/expression_skip_magic_trailing_comma.diff +++ b/tests/data/expression_skip_magic_trailing_comma.diff @@ -11,7 +11,17 @@ True False 1 -@@ -29,63 +29,84 @@ +@@ -21,71 +21,92 @@ + Name1 or (Name2 and Name3) or Name4 + Name1 or Name2 and Name3 or Name4 + v1 << 2 + 1 >> v2 + 1 % finished +-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 +-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) ++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) + not great ~great +value -1 @@ -19,7 +29,7 @@ (~int) and (not ((v1 ^ (123 + v2)) | True)) -+really ** -confusing ** ~operator ** -precedence -flags & ~ select.EPOLLIN and waiters.write_task is not None -++(really ** -(confusing ** ~(operator ** -precedence))) +++(really ** -(confusing ** ~(operator**-precedence))) +flags & ~select.EPOLLIN and waiters.write_task is not None lambda arg: None lambda a=True: a @@ -76,15 +86,19 @@ + *more, +] {i for i in (1, 2, 3)} - {(i ** 2) for i in (1, 2, 3)} +-{(i ** 2) for i in (1, 2, 3)} -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))} -+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} - {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} +-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} ++{(i**2) for i in (1, 2, 3)} ++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} ++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} [i for i in (1, 2, 3)] - [(i ** 2) for i in (1, 2, 3)] +-[(i ** 2) for i in (1, 2, 3)] -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))] -+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] - [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] +-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] ++[(i**2) for i in (1, 2, 3)] ++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] ++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))} +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} @@ -164,10 +178,12 @@ SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) - ((i ** 2) for i in (1, 2, 3)) +-((i ** 2) for i in (1, 2, 3)) -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))) -+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) - (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) +-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) ++((i**2) for i in (1, 2, 3)) ++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) ++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) (*starred,) -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs} +{ @@ -384,13 +400,13 @@ + return True +if ( + ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -+ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n ++ | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n +): + return True +if ( + ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -+ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n ++ ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n +): + return True +if ( @@ -400,7 +416,7 @@ + | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k -+ >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ++ >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +): + return True +( diff --git a/tests/data/pep_572.py b/tests/data/pep_572.py index c6867f26258..d41805f1cb1 100644 --- a/tests/data/pep_572.py +++ b/tests/data/pep_572.py @@ -4,7 +4,7 @@ pass if match := pattern.search(data): pass -[y := f(x), y ** 2, y ** 3] +[y := f(x), y**2, y**3] filtered_data = [y for x in data if (y := f(x)) is None] (y := f(x)) y0 = (y1 := f(x)) diff --git a/tests/data/pep_572_py39.py b/tests/data/pep_572_py39.py index 7bbd5091197..b8b081b8c45 100644 --- a/tests/data/pep_572_py39.py +++ b/tests/data/pep_572_py39.py @@ -1,7 +1,7 @@ # Unparenthesized walruses are now allowed in set literals & set comprehensions # since Python 3.9 {x := 1, 2, 3} -{x4 := x ** 5 for x in range(7)} +{x4 := x**5 for x in range(7)} # We better not remove the parentheses here (since it's a 3.10 feature) x[(a := 1)] x[(a := 1), (b := 3)] diff --git a/tests/data/power_op_spacing.py b/tests/data/power_op_spacing.py new file mode 100644 index 00000000000..87dde7f39dc --- /dev/null +++ b/tests/data/power_op_spacing.py @@ -0,0 +1,103 @@ +def function(**kwargs): + t = a**2 + b**3 + return t ** 2 + + +def function_replace_spaces(**kwargs): + t = a **2 + b** 3 + c ** 4 + + +def function_dont_replace_spaces(): + {**a, **b, **c} + + +a = 5**~4 +b = 5 ** f() +c = -(5**2) +d = 5 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5 +g = a.b**c.d +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 +k = [(2**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2**63], [1, 2**63])] +n = count <= 10**5 +o = settings(max_examples=10**6) +p = {(k, k**2): v**2 for k, v in pairs} +q = [10**i for i in range(6)] +r = x**y + +a = 5.0**~4.0 +b = 5.0 ** f() +c = -(5.0**2.0) +d = 5.0 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5.0 +g = a.b**c.d +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 +k = [(2.0**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2.0**63.0], [1.0, 2**63.0])] +n = count <= 10**5.0 +o = settings(max_examples=10**6.0) +p = {(k, k**2): v**2.0 for k, v in pairs} +q = [10.5**i for i in range(6)] + + +# output + + +def function(**kwargs): + t = a**2 + b**3 + return t**2 + + +def function_replace_spaces(**kwargs): + t = a**2 + b**3 + c**4 + + +def function_dont_replace_spaces(): + {**a, **b, **c} + + +a = 5**~4 +b = 5 ** f() +c = -(5**2) +d = 5 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5 +g = a.b**c.d +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 +k = [(2**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2**63], [1, 2**63])] +n = count <= 10**5 +o = settings(max_examples=10**6) +p = {(k, k**2): v**2 for k, v in pairs} +q = [10**i for i in range(6)] +r = x**y + +a = 5.0**~4.0 +b = 5.0 ** f() +c = -(5.0**2.0) +d = 5.0 ** f["hi"] +e = lazy(lambda **kwargs: 5) +f = f() ** 5.0 +g = a.b**c.d +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 +k = [(2.0**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2.0**63.0], [1.0, 2**63.0])] +n = count <= 10**5.0 +o = settings(max_examples=10**6.0) +p = {(k, k**2): v**2.0 for k, v in pairs} +q = [10.5**i for i in range(6)] diff --git a/tests/data/slices.py b/tests/data/slices.py index 7a42678f646..165117cdcb4 100644 --- a/tests/data/slices.py +++ b/tests/data/slices.py @@ -9,7 +9,7 @@ slice[:c, c - 1] slice[c, c + 1, d::] slice[ham[c::d] :: 1] -slice[ham[cheese ** 2 : -1] : 1 : 1, ham[1:2]] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] slice[:-1:] slice[lambda: None : lambda: None] slice[lambda x, y, *args, really=2, **kwargs: None :, None::] diff --git a/tests/test_format.py b/tests/test_format.py index 3895a095e86..c6c811040dc 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -48,6 +48,7 @@ "function2", "function_trailing_comma", "import_spacing", + "power_op_spacing", "remove_parens", "slices", "string_prefixes", From 32dd9ecb2e9dec8b29c07726d5713ed5b4c36547 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Jan 2022 15:58:58 -0800 Subject: [PATCH 17/50] properly run ourselves twice (#2807) The previous run-twice logic only affected the stability checks but not the output. Now, we actually output the twice-formatted code. --- CHANGES.md | 2 + src/black/__init__.py | 29 +++++++------- tests/data/trailing_comma_optional_parens1.py | 12 ++++++ tests/data/trailing_comma_optional_parens2.py | 16 +++++++- tests/data/trailing_comma_optional_parens3.py | 18 ++++++++- tests/test_black.py | 39 ------------------- tests/test_format.py | 3 ++ 7 files changed, 65 insertions(+), 54 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d203896a801..37990686508 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,8 @@ - Enable Python 3.10+ by default, without any extra need to specify `--target-version=py310`. (#2758) - Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804) +- Work around bug that causes unstable formatting in some cases in the presence of the + magic trailing comma (#2807) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 7024c9d52b0..769e693ed23 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -968,17 +968,7 @@ def check_stability_and_equivalence( content differently. """ assert_equivalent(src_contents, dst_contents) - - # Forced second pass to work around optional trailing commas (becoming - # forced trailing commas on pass 2) interacting differently with optional - # parentheses. Admittedly ugly. - dst_contents_pass2 = format_str(dst_contents, mode=mode) - if dst_contents != dst_contents_pass2: - dst_contents = dst_contents_pass2 - assert_equivalent(src_contents, dst_contents, pass_num=2) - assert_stable(src_contents, dst_contents, mode=mode) - # Note: no need to explicitly call `assert_stable` if `dst_contents` was - # the same as `dst_contents_pass2`. + assert_stable(src_contents, dst_contents, mode=mode) def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: @@ -1108,7 +1098,7 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon raise NothingChanged -def format_str(src_contents: str, *, mode: Mode) -> FileContent: +def format_str(src_contents: str, *, mode: Mode) -> str: """Reformat a string and return new contents. `mode` determines formatting options, such as how many characters per line are @@ -1138,6 +1128,16 @@ def f( hey """ + dst_contents = _format_str_once(src_contents, mode=mode) + # Forced second pass to work around optional trailing commas (becoming + # forced trailing commas on pass 2) interacting differently with optional + # parentheses. Admittedly ugly. + if src_contents != dst_contents: + return _format_str_once(dst_contents, mode=mode) + return dst_contents + + +def _format_str_once(src_contents: str, *, mode: Mode) -> str: src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) dst_contents = [] future_imports = get_future_imports(src_node) @@ -1367,7 +1367,10 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: def assert_stable(src: str, dst: str, mode: Mode) -> None: """Raise AssertionError if `dst` reformats differently the second time.""" - newdst = format_str(dst, mode=mode) + # We shouldn't call format_str() here, because that formats the string + # twice and may hide a bug where we bounce back and forth between two + # versions. + newdst = _format_str_once(dst, mode=mode) if dst != newdst: log = dump_to_file( str(mode), diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/trailing_comma_optional_parens1.py index 5ad29a8affd..f5be2f24cf4 100644 --- a/tests/data/trailing_comma_optional_parens1.py +++ b/tests/data/trailing_comma_optional_parens1.py @@ -1,3 +1,15 @@ if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): + pass + +# output + +if ( + e1234123412341234.winerror + not in ( + _winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY, + ) + or _check_timeout(t) +): pass \ No newline at end of file diff --git a/tests/data/trailing_comma_optional_parens2.py b/tests/data/trailing_comma_optional_parens2.py index 2817073816e..1dfb54ca687 100644 --- a/tests/data/trailing_comma_optional_parens2.py +++ b/tests/data/trailing_comma_optional_parens2.py @@ -1,3 +1,17 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or (8, 5, 8) <= get_tk_patchlevel() < (8, 6)): - pass \ No newline at end of file + pass + +# output + +if ( + e123456.get_tk_patchlevel() >= (8, 6, 0, "final") + or ( + 8, + 5, + 8, + ) + <= get_tk_patchlevel() + < (8, 6) +): + pass diff --git a/tests/data/trailing_comma_optional_parens3.py b/tests/data/trailing_comma_optional_parens3.py index e6a673ec537..bccf47430a7 100644 --- a/tests/data/trailing_comma_optional_parens3.py +++ b/tests/data/trailing_comma_optional_parens3.py @@ -5,4 +5,20 @@ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} \ No newline at end of file + ) % {"reported_username": reported_username, "report_reason": report_reason} + + +# output + + +if True: + if True: + if True: + return _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) % { + "reported_username": reported_username, + "report_reason": report_reason, + } \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 8d691d2f019..2dd284f2cd6 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -228,45 +228,6 @@ def _test_wip(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, black.FileMode()) - @unittest.expectedFailure - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability1(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens1") - actual = fs(source) - black.assert_stable(source, actual, DEFAULT_MODE) - - @unittest.expectedFailure - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability2(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens2") - actual = fs(source) - black.assert_stable(source, actual, DEFAULT_MODE) - - @unittest.expectedFailure - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability3(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens3") - actual = fs(source) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability1_pass2(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens1") - actual = fs(fs(source)) # this is what `format_file_contents` does with --safe - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability2_pass2(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens2") - actual = fs(fs(source)) # this is what `format_file_contents` does with --safe - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_trailing_comma_optional_parens_stability3_pass2(self) -> None: - source, _expected = read_data("trailing_comma_optional_parens3") - actual = fs(fs(source)) # this is what `format_file_contents` does with --safe - black.assert_stable(source, actual, DEFAULT_MODE) - def test_pep_572_version_detection(self) -> None: source, _ = read_data("pep_572") root = black.lib2to3_parse(source) diff --git a/tests/test_format.py b/tests/test_format.py index c6c811040dc..a4619b4a652 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -52,6 +52,9 @@ "remove_parens", "slices", "string_prefixes", + "trailing_comma_optional_parens1", + "trailing_comma_optional_parens2", + "trailing_comma_optional_parens3", "tricky_unicode_symbols", "tupleassign", ] From 889a8d5dd27a73aa780e989a850bbdaaa9946a13 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 26 Jan 2022 16:47:36 -0800 Subject: [PATCH 18/50] Fix crash on some power hugging cases (#2806) Found by the fuzzer. Repro case: python -m black -c 'importA;()<<0**0#' --- src/black/linegen.py | 2 ++ src/black/lines.py | 4 +++- src/blib2to3/pytree.py | 6 +++++- tests/data/power_op_newline.py | 10 ++++++++++ tests/test_format.py | 6 ++++++ 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/data/power_op_newline.py diff --git a/src/black/linegen.py b/src/black/linegen.py index 9fbdfadba6a..ac60ed1986d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -942,6 +942,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf if ( prev and prev.type == token.COMMA + and leaf.opening_bracket is not None and not is_one_tuple_between( leaf.opening_bracket, leaf, line.leaves ) @@ -969,6 +970,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf if ( prev and prev.type == token.COMMA + and leaf.opening_bracket is not None and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) ): # Never omit bracket pairs with trailing commas. diff --git a/src/black/lines.py b/src/black/lines.py index c602aa69ce9..7d50f02aebc 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -277,7 +277,9 @@ def has_magic_trailing_comma( if self.is_import: return True - if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves): + if closing.opening_bracket is not None and not is_one_tuple_between( + closing.opening_bracket, closing, self.leaves + ): return True return False diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index bd86270b8e2..b203ce5b2ac 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -386,7 +386,8 @@ class Leaf(Base): value: Text fixers_applied: List[Any] bracket_depth: int - opening_bracket: "Leaf" + # Changed later in brackets.py + opening_bracket: Optional["Leaf"] = None used_names: Optional[Set[Text]] _prefix = "" # Whitespace and comments preceding this token in the input lineno: int = 0 # Line where this token starts in the input @@ -399,6 +400,7 @@ def __init__( context: Optional[Context] = None, prefix: Optional[Text] = None, fixers_applied: List[Any] = [], + opening_bracket: Optional["Leaf"] = None, ) -> None: """ Initializer. @@ -416,6 +418,7 @@ def __init__( self._prefix = prefix self.fixers_applied: Optional[List[Any]] = fixers_applied[:] self.children = [] + self.opening_bracket = opening_bracket def __repr__(self) -> str: """Return a canonical string representation.""" @@ -448,6 +451,7 @@ def clone(self) -> "Leaf": self.value, (self.prefix, (self.lineno, self.column)), fixers_applied=self.fixers_applied, + opening_bracket=self.opening_bracket, ) def leaves(self) -> Iterator["Leaf"]: diff --git a/tests/data/power_op_newline.py b/tests/data/power_op_newline.py new file mode 100644 index 00000000000..85d434d63f6 --- /dev/null +++ b/tests/data/power_op_newline.py @@ -0,0 +1,10 @@ +importA;()<<0**0# + +# output + +importA +( + () + << 0 + ** 0 +) # diff --git a/tests/test_format.py b/tests/test_format.py index a4619b4a652..88f084ea478 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -256,3 +256,9 @@ def test_python38() -> None: def test_python39() -> None: source, expected = read_data("python39") assert_format(source, expected, minimum_version=(3, 9)) + + +def test_power_op_newline() -> None: + # requires line_length=0 + source, expected = read_data("power_op_newline") + assert_format(source, expected, mode=black.Mode(line_length=0)) From b517dfb396a82ef263f0d366c4dc107451cf0c3c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 26 Jan 2022 17:18:43 -0800 Subject: [PATCH 19/50] black-primer: stop running it (#2809) At the moment, it's just a source of spurious CI failures and busywork updating the configuration file. Unlike diff-shades, it is run across many different platforms and Python versions, but that doesn't seem essential. We already run unit tests across platforms and versions. I chose to leave the code around for now in case somebody is using it, but CI will no longer run it. --- .github/workflows/primer.yml | 47 -------------------------- .github/workflows/uvloop_test.yml | 4 +-- CHANGES.md | 1 + README.md | 1 - docs/contributing/gauging_changes.md | 49 ++++------------------------ docs/contributing/the_basics.md | 15 --------- 6 files changed, 10 insertions(+), 107 deletions(-) delete mode 100644 .github/workflows/primer.yml diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml deleted file mode 100644 index 5fa6ac066e3..00000000000 --- a/.github/workflows/primer.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Primer - -on: - push: - paths-ignore: - - "docs/**" - - "*.md" - - pull_request: - paths-ignore: - - "docs/**" - - "*.md" - -jobs: - build: - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. Without this if check, checks are duplicated since - # internal PRs match both the push and pull_request events. - if: - github.event_name == 'push' || github.event.pull_request.head.repo.full_name != - github.repository - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] - os: [ubuntu-latest, windows-latest] - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[d,jupyter]" - - - name: Primer run - env: - pythonioencoding: utf-8 - run: | - black-primer diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml index 5d23ec64299..a639bbd1b97 100644 --- a/.github/workflows/uvloop_test.yml +++ b/.github/workflows/uvloop_test.yml @@ -40,6 +40,6 @@ jobs: run: | python -m pip install -e ".[uvloop]" - - name: Primer uvloop run + - name: Format ourselves run: | - black-primer + python -m black --check src/ diff --git a/CHANGES.md b/CHANGES.md index 37990686508..0dc4952f069 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -42,6 +42,7 @@ - Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804) - Work around bug that causes unstable formatting in some cases in the presence of the magic trailing comma (#2807) +- Deprecate the `black-primer` tool (#2809) ### Packaging diff --git a/README.md b/README.md index e900d2d75a2..a00495c8858 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@

Actions Status -Actions Status Documentation Status Coverage Status License: MIT diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index 9b38fe1b628..59c40eb3909 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -9,51 +9,16 @@ enough to cause frustration to projects that are already "black formatted". ## black-primer -`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of -Git accessible projects in parallel. (configured in `primer.json`) _(A PR will be -accepted to add Mercurial support.)_ - -### Run flow - -- Ensure we have a `black` + `git` in PATH -- Load projects from `primer.json` -- Run projects in parallel with `--worker` workers (defaults to CPU count / 2) - - Checkout projects - - Run black and record result - - Clean up repository checkout _(can optionally be disabled via `--keep`)_ -- Display results summary to screen -- Default to cleaning up `--work-dir` (which defaults to tempfile schemantics) -- Return - - 0 for successful run - - \< 0 for environment / internal error - - \> 0 for each project with an error - -### Speed up runs 🏎 - -If you're running locally yourself to test black on lots of code try: - -- Using `-k` / `--keep` + `-w` / `--work-dir` so you don't have to re-checkout the repo - each run - -### CLI arguments - -```{program-output} black-primer --help - -``` +`black-primer` is an obsolete tool (now replaced with `diff-shades`) that was used to +gauge the impact of changes in _Black_ on open-source code. It is no longer used +internally and will be removed from the _Black_ repository in the future. ## diff-shades -diff-shades is a tool similar to black-primer, it also runs _Black_ across a list of Git -cloneable OSS projects recording the results. The intention is to eventually fully -replace black-primer with diff-shades as it's much more feature complete and supports -our needs better. - -The main highlight feature of diff-shades is being able to compare two revisions of -_Black_. This is incredibly useful as it allows us to see what exact changes will occur, -say merging a certain PR. Black-primer's results would usually be filled with changes -caused by pre-existing code in Black drowning out the (new) changes we want to see. It -operates similarly to black-primer but crucially it saves the results as a JSON file -which allows for the rich comparison features alluded to above. +diff-shades is a tool that runs _Black_ across a list of Git cloneable OSS projects +recording the results. The main highlight feature of diff-shades is being able to +compare two revisions of _Black_. This is incredibly useful as it allows us to see what +exact changes will occur, say merging a certain PR. For more information, please see the [diff-shades documentation][diff-shades]. diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 23fbb8a3d7e..9325a9e44ed 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -30,9 +30,6 @@ the root of the black repo: # Optional Fuzz testing (.venv)$ tox -e fuzz - -# Optional CI run to test your changes on many popular python projects -(.venv)$ black-primer [-k -w /tmp/black_test_repos] ``` ### News / Changelog Requirement @@ -69,18 +66,6 @@ If you make changes to docs, you can test they still build locally too. (.venv)$ sphinx-build -a -b html -W docs/ docs/_build/ ``` -## black-primer - -`black-primer` is used by CI to pull down well-known _Black_ formatted projects and see -if we get source code changes. It will error on formatting changes or errors. Please run -before pushing your PR to see if you get the actions you would expect from _Black_ with -your PR. You may need to change -[primer.json](https://github.com/psf/black/blob/main/src/black_primer/primer.json) -configuration for it to pass. - -For more `black-primer` information visit the -[documentation](./gauging_changes.md#black-primer). - ## Hygiene If you're fixing a bug, add a test. Run it first to confirm it fails, then fix the bug, From b92822afeedd45daa3b1d094a502daf936f7fa9d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 26 Jan 2022 19:44:39 -0800 Subject: [PATCH 20/50] more trailing comma tests (#2810) --- tests/data/trailing_comma_optional_parens1.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/trailing_comma_optional_parens1.py index f5be2f24cf4..f9f4ae5e023 100644 --- a/tests/data/trailing_comma_optional_parens1.py +++ b/tests/data/trailing_comma_optional_parens1.py @@ -2,6 +2,25 @@ _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass + +class X: + def get_help_text(self): + return ngettext( + "Your password must contain at least %(min_length)d character.", + "Your password must contain at least %(min_length)d characters.", + self.min_length, + ) % {'min_length': self.min_length} + +class A: + def b(self): + if self.connection.mysql_is_mariadb and ( + 10, + 4, + 3, + ) < self.connection.mysql_version < (10, 5, 2): + pass + + # output if ( @@ -12,4 +31,31 @@ ) or _check_timeout(t) ): - pass \ No newline at end of file + pass + + +class X: + def get_help_text(self): + return ( + ngettext( + "Your password must contain at least %(min_length)d character.", + "Your password must contain at least %(min_length)d characters.", + self.min_length, + ) + % {"min_length": self.min_length} + ) + + +class A: + def b(self): + if ( + self.connection.mysql_is_mariadb + and ( + 10, + 4, + 3, + ) + < self.connection.mysql_version + < (10, 5, 2) + ): + pass From 777cae55b601f8a501e2138cec99361929b128ea Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 28 Jan 2022 11:01:50 +0530 Subject: [PATCH 21/50] Use parentheses on method access on float and int literals (#2799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jelle Zijlstra Co-authored-by: Felix Hildén --- CHANGES.md | 3 ++ src/black/linegen.py | 22 +++++++++ src/black/nodes.py | 7 +-- .../attribute_access_on_number_literals.py | 47 +++++++++++++++++++ tests/data/expression.diff | 9 ++-- tests/data/expression.py | 4 +- .../expression_skip_magic_trailing_comma.diff | 9 ++-- tests/test_format.py | 1 + 8 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 tests/data/attribute_access_on_number_literals.py diff --git a/CHANGES.md b/CHANGES.md index 0dc4952f069..6966a91aa11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -42,6 +42,9 @@ - Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804) - Work around bug that causes unstable formatting in some cases in the presence of the magic trailing comma (#2807) +- Use parentheses for attribute access on decimal float and int literals (#2799) +- Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex + literals (#2799) - Deprecate the `black-primer` tool (#2809) ### Packaging diff --git a/src/black/linegen.py b/src/black/linegen.py index ac60ed1986d..b572ed0b52f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -197,6 +197,28 @@ def visit_decorators(self, node: Node) -> Iterator[Line]: yield from self.line() yield from self.visit(child) + def visit_power(self, node: Node) -> Iterator[Line]: + for idx, leaf in enumerate(node.children[:-1]): + next_leaf = node.children[idx + 1] + + if not isinstance(leaf, Leaf): + continue + + value = leaf.value.lower() + if ( + leaf.type == token.NUMBER + and next_leaf.type == syms.trailer + # Ensure that we are in an attribute trailer + and next_leaf.children[0].type == token.DOT + # It shouldn't wrap hexadecimal, binary and octal literals + and not value.startswith(("0x", "0b", "0o")) + # It shouldn't wrap complex literals + and "j" not in value + ): + wrap_in_parentheses(node, leaf) + + yield from self.visit_default(node) + def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: """Remove a semicolon and put the other statement on a separate line.""" yield from self.line() diff --git a/src/black/nodes.py b/src/black/nodes.py index 74dfa896295..51d4cb8618d 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -306,12 +306,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 return NO if not prev: - if t == token.DOT: - prevp = preceding_leaf(p) - if not prevp or prevp.type != token.NUMBER: - return NO - - elif t == token.LSQB: + if t == token.DOT or t == token.LSQB: return NO elif prev.type != token.COMMA: diff --git a/tests/data/attribute_access_on_number_literals.py b/tests/data/attribute_access_on_number_literals.py new file mode 100644 index 00000000000..7c16bdfb3a5 --- /dev/null +++ b/tests/data/attribute_access_on_number_literals.py @@ -0,0 +1,47 @@ +x = 123456789 .bit_count() +x = (123456).__abs__() +x = .1.is_integer() +x = 1. .imag +x = 1E+1.imag +x = 1E-1.real +x = 123456789.123456789.hex() +x = 123456789.123456789E123456789 .real +x = 123456789E123456789 .conjugate() +x = 123456789J.real +x = 123456789.123456789J.__add__(0b1011.bit_length()) +x = 0XB1ACC.conjugate() +x = 0B1011 .conjugate() +x = 0O777 .real +x = 0.000000006 .hex() +x = -100.0000J + +if 10 .real: + ... + +y = 100[no] +y = 100(no) + +# output + +x = (123456789).bit_count() +x = (123456).__abs__() +x = (0.1).is_integer() +x = (1.0).imag +x = (1e1).imag +x = (1e-1).real +x = (123456789.123456789).hex() +x = (123456789.123456789e123456789).real +x = (123456789e123456789).conjugate() +x = 123456789j.real +x = 123456789.123456789j.__add__(0b1011.bit_length()) +x = 0xB1ACC.conjugate() +x = 0b1011.conjugate() +x = 0o777.real +x = (0.000000006).hex() +x = -100.0000j + +if (10).real: + ... + +y = 100[no] +y = 100(no) diff --git a/tests/data/expression.diff b/tests/data/expression.diff index 5f29a18dc7f..2eaaeb479f8 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -11,7 +11,7 @@ True False 1 -@@ -21,71 +21,104 @@ +@@ -21,99 +21,135 @@ Name1 or (Name2 and Name3) or Name4 Name1 or Name2 and Name3 or Name4 v1 << 2 @@ -144,8 +144,11 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -94,26 +127,29 @@ - 1.0 .real + call.me(maybe) +-1 .real +-1.0 .real ++(1).real ++(1.0).real ....__class__ list[str] dict[str, int] diff --git a/tests/data/expression.py b/tests/data/expression.py index b056841027d..06096c589f1 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -382,8 +382,8 @@ async def f(): call(b, **self.screen_kwargs) lukasz.langa.pl call.me(maybe) -1 .real -1.0 .real +(1).real +(1.0).real ....__class__ list[str] dict[str, int] diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff index 5b722c91352..eba3fd2da7d 100644 --- a/tests/data/expression_skip_magic_trailing_comma.diff +++ b/tests/data/expression_skip_magic_trailing_comma.diff @@ -11,7 +11,7 @@ True False 1 -@@ -21,71 +21,92 @@ +@@ -21,99 +21,118 @@ Name1 or (Name2 and Name3) or Name4 Name1 or Name2 and Name3 or Name4 v1 << 2 @@ -132,8 +132,11 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -94,26 +115,24 @@ - 1.0 .real + call.me(maybe) +-1 .real +-1.0 .real ++(1).real ++(1.0).real ....__class__ list[str] dict[str, int] diff --git a/tests/test_format.py b/tests/test_format.py index 88f084ea478..aef22545f5b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -15,6 +15,7 @@ ) SIMPLE_CASES: List[str] = [ + "attribute_access_on_number_literals", "beginning_backslash", "bracketmatch", "class_blank_parentheses", From fda2561f79e10826dbdeb900b6124d642766229f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 28 Jan 2022 00:16:25 -0800 Subject: [PATCH 22/50] Tests for unicode identifiers (#2816) --- fuzz.py | 2 +- tests/data/tricky_unicode_symbols.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/fuzz.py b/fuzz.py index 09a86a2f571..f5f655ea279 100644 --- a/fuzz.py +++ b/fuzz.py @@ -48,7 +48,7 @@ def test_idempotent_any_syntatically_valid_python( dst_contents = black.format_str(src_contents, mode=mode) except black.InvalidInput: # This is a bug - if it's valid Python code, as above, Black should be - # able to cope with it. See issues #970, #1012, #1358, and #1557. + # able to cope with it. See issues #970, #1012 # TODO: remove this try-except block when issues are resolved. return except TokenError as e: diff --git a/tests/data/tricky_unicode_symbols.py b/tests/data/tricky_unicode_symbols.py index 366a92fa9d4..ad8b6108590 100644 --- a/tests/data/tricky_unicode_symbols.py +++ b/tests/data/tricky_unicode_symbols.py @@ -4,3 +4,6 @@ x󠄀 = 4 មុ = 1 Q̇_per_meter = 4 + +A᧚ = 3 +A፩ = 8 From 5f01b872e0553e17af1543ea27e500f79f716a29 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 28 Jan 2022 06:25:24 -0800 Subject: [PATCH 23/50] reorganize release notes for 22.1.0 (#2790) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 79 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6966a91aa11..f81c285d0be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,50 +2,71 @@ ## Unreleased -### _Black_ +At long last, _Black_ is no longer a beta product! This is the first non-beta release +and the first release covered by our new stability policy. + +### Highlights - **Remove Python 2 support** (#2740) -- Do not accept bare carriage return line endings in pyproject.toml (#2408) -- Improve error message for invalid regular expression (#2678) -- Improve error message when parsing fails during AST safety check by embedding the - underlying SyntaxError (#2693) +- Introduce the `--preview` flag (#2752) + +### Style + +- Deprecate `--experimental-string-processing` and move the functionality under + `--preview` (#2789) +- For stubs, one blank line between class attributes and methods is now kept if there's + at least one pre-existing blank line (#2736) +- Black now normalizes string prefix order (#2297) +- Remove spaces around power operators if both operands are simple (#2726) +- Work around bug that causes unstable formatting in some cases in the presence of the + magic trailing comma (#2807) +- Use parentheses for attribute access on decimal float and int literals (#2799) +- Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex + literals (#2799) + +### Parser + - Fix mapping cases that contain as-expressions, like `case {"key": 1 | 2 as password}` (#2686) - Fix cases that contain multiple top-level as-expressions, like `case 1 as a, 2 as b` (#2716) - Fix call patterns that contain as-expressions with keyword arguments, like `case Foo(bar=baz as quux)` (#2749) -- No longer color diff headers white as it's unreadable in light themed terminals - (#2691) - Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700) - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) -- Remove spaces around power operators if both operands are simple (#2726) -- Allow setting custom cache directory on all platforms with environment variable - `BLACK_CACHE_DIR` (#2739). -- Text coloring added in the final statistics (#2712) -- For stubs, one blank line between class attributes and methods is now kept if there's - at least one pre-existing blank line (#2736) -- Verbose mode also now describes how a project root was discovered and which paths will - be formatted. (#2526) -- Speed-up the new backtracking parser about 4X in general (enabled when - `--target-version` is set to 3.10 and higher). (#2728) - Fix handling of standalone `match()` or `case()` when there is a trailing newline or a comment inside of the parentheses. (#2760) -- Black now normalizes string prefix order (#2297) + +### Performance + +- Speed-up the new backtracking parser about 4X in general (enabled when + `--target-version` is set to 3.10 and higher). (#2728) +- _Black_ is now compiled with [mypyc](https://github.com/mypyc/mypyc) for an overall 2x + speed-up. 64-bit Windows, MacOS, and Linux (not including musl) are supported. (#1009, + #2431) + +### Configuration + +- Do not accept bare carriage return line endings in pyproject.toml (#2408) - Add configuration option (`python-cell-magics`) to format cells with custom magics in Jupyter Notebooks (#2744) -- Deprecate `--experimental-string-processing` and move the functionality under - `--preview` (#2789) +- Allow setting custom cache directory on all platforms with environment variable + `BLACK_CACHE_DIR` (#2739). - Enable Python 3.10+ by default, without any extra need to specify `--target-version=py310`. (#2758) - Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804) -- Work around bug that causes unstable formatting in some cases in the presence of the - magic trailing comma (#2807) -- Use parentheses for attribute access on decimal float and int literals (#2799) -- Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex - literals (#2799) -- Deprecate the `black-primer` tool (#2809) + +### Output + +- Improve error message for invalid regular expression (#2678) +- Improve error message when parsing fails during AST safety check by embedding the + underlying SyntaxError (#2693) +- No longer color diff headers white as it's unreadable in light themed terminals + (#2691) +- Text coloring added in the final statistics (#2712) +- Verbose mode also now describes how a project root was discovered and which paths will + be formatted. (#2526) ### Packaging @@ -53,11 +74,6 @@ - `typing-extensions` is no longer a required dependency in Python 3.10+ (#2772) - Set `click` lower bound to `8.0.0` (#2791) -### Preview style - -- Introduce the `--preview` flag (#2752) -- Add `--experimental-string-processing` to the preview style (#2789) - ### Integrations - Update GitHub action to support containerized runs (#2748) @@ -67,6 +83,7 @@ - Change protocol in pip installation instructions to `https://` (#2761) - Change HTML theme to Furo primarily for its responsive design and mobile support (#2793) +- Deprecate the `black-primer` tool (#2809) ## 21.12b0 From e1506769a428889bc66964edabf76476433c031a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Fri, 28 Jan 2022 20:58:17 +0200 Subject: [PATCH 24/50] Elaborate on Python support policy (#2819) --- CHANGES.md | 1 + docs/faq.md | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f81c285d0be..440aaeaa35a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -84,6 +84,7 @@ and the first release covered by our new stability policy. - Change HTML theme to Furo primarily for its responsive design and mobile support (#2793) - Deprecate the `black-primer` tool (#2809) +- Document Python support policy (#2819) ## 21.12b0 diff --git a/docs/faq.md b/docs/faq.md index 0cff6ae5e1d..264141e3f39 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -71,9 +71,16 @@ readability because operators are misaligned. Disable W503 and enable the disabled-by-default counterpart W504. E203 should be disabled while changes are still [discussed](https://github.com/PyCQA/pycodestyle/issues/373). -## Does Black support Python 2? +## Which Python versions does Black support? -Support for formatting Python 2 code was removed in version 22.0. +Currently the runtime requires Python 3.6-3.10. Formatting is supported for files +containing syntax from Python 3.3 to 3.10. We promise to support at least all Python +versions that have not reached their end of life. This is the case for both running +_Black_ and formatting code. + +Support for formatting Python 2 code was removed in version 22.0. While we've made no +plans to stop supporting older Python 3 minor versions immediately, their support might +also be removed some time in the future without a deprecation period. ## Why does my linter or typechecker complain after I format my code? From 343795029f0d3ffa2f04ca5074a18861b2831d39 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 28 Jan 2022 16:29:07 -0800 Subject: [PATCH 25/50] Treat blank lines in stubs the same inside top-level `if` statements (#2820) --- CHANGES.md | 1 + src/black/lines.py | 10 +++-- tests/data/stub.pyi | 93 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 440aaeaa35a..274c5640ec0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ and the first release covered by our new stability policy. - Use parentheses for attribute access on decimal float and int literals (#2799) - Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex literals (#2799) +- Treat blank lines in stubs the same inside top-level `if` statements (#2820) ### Parser diff --git a/src/black/lines.py b/src/black/lines.py index 7d50f02aebc..1c4e38a96c1 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -530,11 +530,11 @@ def _maybe_empty_lines_for_class_or_def( return 0, 0 if self.is_pyi: - if self.previous_line.depth > current_line.depth: - newlines = 0 if current_line.depth else 1 - elif current_line.is_class or self.previous_line.is_class: - if current_line.depth: + if current_line.is_class or self.previous_line.is_class: + if self.previous_line.depth < current_line.depth: newlines = 0 + elif self.previous_line.depth > current_line.depth: + newlines = 1 elif current_line.is_stub_class and self.previous_line.is_stub_class: # No blank line between classes with an empty body newlines = 0 @@ -551,6 +551,8 @@ def _maybe_empty_lines_for_class_or_def( # Blank line between a block of functions (maybe with preceding # decorators) and a block of non-functions newlines = 1 + elif self.previous_line.depth > current_line.depth: + newlines = 1 else: newlines = 0 else: diff --git a/tests/data/stub.pyi b/tests/data/stub.pyi index 9a246211284..af2cd2c2c02 100644 --- a/tests/data/stub.pyi +++ b/tests/data/stub.pyi @@ -32,6 +32,48 @@ def g(): def h(): ... +if sys.version_info >= (3, 8): + class E: + def f(self): ... + class F: + + def f(self): ... + class G: ... + class H: ... +else: + class I: ... + class J: ... + def f(): ... + + class K: + def f(self): ... + def f(): ... + +class Nested: + class dirty: ... + class little: ... + class secret: + def who_has_to_know(self): ... + def verse(self): ... + +class Conditional: + def f(self): ... + if sys.version_info >= (3, 8): + def g(self): ... + else: + def g(self): ... + def h(self): ... + def i(self): ... + if sys.version_info >= (3, 8): + def j(self): ... + def k(self): ... + if sys.version_info >= (3, 8): + class A: ... + class B: ... + class C: + def l(self): ... + def m(self): ... + # output X: int @@ -56,3 +98,54 @@ class A: def g(): ... def h(): ... + +if sys.version_info >= (3, 8): + class E: + def f(self): ... + + class F: + def f(self): ... + + class G: ... + class H: ... + +else: + class I: ... + class J: ... + + def f(): ... + + class K: + def f(self): ... + + def f(): ... + +class Nested: + class dirty: ... + class little: ... + + class secret: + def who_has_to_know(self): ... + + def verse(self): ... + +class Conditional: + def f(self): ... + if sys.version_info >= (3, 8): + def g(self): ... + else: + def g(self): ... + + def h(self): ... + def i(self): ... + if sys.version_info >= (3, 8): + def j(self): ... + + def k(self): ... + if sys.version_info >= (3, 8): + class A: ... + class B: ... + + class C: + def l(self): ... + def m(self): ... From 4ce049dbfa8ddd00bff3656cbca6ecf5f85c413e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 28 Jan 2022 16:48:38 -0800 Subject: [PATCH 26/50] torture test (#2815) Fixes #2651. Fixes #2754. Fixes #2518. Fixes #2321. This adds a test that lists a number of cases of unstable formatting that we have seen in the issue tracker. Checking it in will ensure that we don't regress on these cases. --- tests/data/torture.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 2 files changed, 82 insertions(+) create mode 100644 tests/data/torture.py diff --git a/tests/data/torture.py b/tests/data/torture.py new file mode 100644 index 00000000000..79a44c2e34c --- /dev/null +++ b/tests/data/torture.py @@ -0,0 +1,81 @@ +importA;() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # + +assert sort_by_dependency( + { + "1": {"2", "3"}, "2": {"2a", "2b"}, "3": {"3a", "3b"}, + "2a": set(), "2b": set(), "3a": set(), "3b": set() + } +) == ["2a", "2b", "2", "3a", "3b", "3", "1"] + +importA +0;0^0# + +class A: + def foo(self): + for _ in range(10): + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member + xxxxxxxxxxxx + ) + +def test(self, othr): + return (1 == 2 and + (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == + (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + +# output + +importA +( + () + << 0 + ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 +) # + +assert ( + sort_by_dependency( + { + "1": {"2", "3"}, + "2": {"2a", "2b"}, + "3": {"3a", "3b"}, + "2a": set(), + "2b": set(), + "3a": set(), + "3b": set(), + } + ) + == ["2a", "2b", "2", "3a", "3b", "3", "1"] +) + +importA +0 +0 ^ 0 # + + +class A: + def foo(self): + for _ in range(10): + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( + xxxxxxxxxxxx + ) # pylint: disable=no-member + + +def test(self, othr): + return 1 == 2 and ( + name, + description, + self.default, + self.selected, + self.auto_generated, + self.parameters, + self.meta_data, + self.schedule, + ) == ( + name, + description, + othr.default, + othr.selected, + othr.auto_generated, + othr.parameters, + othr.meta_data, + othr.schedule, + ) diff --git a/tests/test_format.py b/tests/test_format.py index aef22545f5b..04676c1c2c5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -53,6 +53,7 @@ "remove_parens", "slices", "string_prefixes", + "torture", "trailing_comma_optional_parens1", "trailing_comma_optional_parens2", "trailing_comma_optional_parens3", From df0aeeeee0378f2d2cdc33cbb38e17c3b8b53bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Sat, 29 Jan 2022 02:49:43 +0200 Subject: [PATCH 27/50] Formalise style preference description (#2818) Closes #1256: I reworded our style docs to be more explicit about the style we're aiming for and how it is changed (or isn't). --- docs/the_black_code_style/current_style.md | 15 +++++++++------ docs/the_black_code_style/index.rst | 4 ++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 5be7ba6dbdb..0bf5894abdd 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -2,10 +2,14 @@ ## Code style -_Black_ reformats entire files in place. Style configuration options are deliberately -limited and rarely added. It doesn't take previous formatting into account, except for -the magic trailing comma and preserving newlines. It doesn't reformat blocks that start -with `# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`. +_Black_ aims for consistency, generality, readability and reducing git diffs. Similar +language constructs are formatted with similar rules. Style configuration options are +deliberately limited and rarely added. Previous formatting is taken into account as +little as possible, with rare exceptions like the magic trailing comma. The coding style +used by _Black_ can be viewed as a strict subset of PEP 8. + +_Black_ reformats entire files in place. It doesn't reformat blocks that start with +`# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`. `# fmt: on/off` have to be on the same level of indentation. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a courtesy for straddling code. @@ -18,8 +22,7 @@ running `black --preview`. _Black_ ignores previous formatting and applies uniform horizontal and vertical whitespace to your code. The rules for horizontal whitespace can be summarized as: do -whatever makes `pycodestyle` happy. The coding style used by _Black_ can be viewed as a -strict subset of PEP 8. +whatever makes `pycodestyle` happy. As for vertical whitespace, _Black_ tries to render one full expression or simple statement per line. If this fits the allotted line length, great. diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst index 3952a174223..511a6ecf099 100644 --- a/docs/the_black_code_style/index.rst +++ b/docs/the_black_code_style/index.rst @@ -12,6 +12,10 @@ The Black Code Style While keeping the style unchanged throughout releases has always been a goal, the *Black* code style isn't set in stone. It evolves to accommodate for new features in the Python language and, occasionally, in response to user feedback. +Large-scale style preferences presented in :doc:`current_style` are very unlikely to +change, but minor style aspects and details might change according to the stability +policy presented below. Ongoing style considerations are tracked on GitHub with the +`design `_ issue label. Stability Policy ---------------- From 95e77cb5590a1499d3aa4cf7fe60481347191c35 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 28 Jan 2022 16:57:05 -0800 Subject: [PATCH 28/50] Fix arithmetic stability issue (#2817) It turns out "simple_stmt" isn't that simple: it can contain multiple statements separated by semicolons. Invisible parenthesis logic for arithmetic expressions only looked at the first child of simple_stmt. This causes instability in the presence of semicolons, since the next run through the statement following the semicolon will be the first child of another simple_stmt. I believe this along with #2572 fix the known stability issues. --- CHANGES.md | 1 + src/black/linegen.py | 10 +++++++--- src/black/nodes.py | 7 +++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 274c5640ec0..b57a360f1bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,7 @@ and the first release covered by our new stability policy. - Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex literals (#2799) - Treat blank lines in stubs the same inside top-level `if` statements (#2820) +- Fix unstable formatting with semicolons and arithmetic expressions (#2817) ### Parser diff --git a/src/black/linegen.py b/src/black/linegen.py index b572ed0b52f..495d3230f8f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -7,7 +7,7 @@ from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS -from black.nodes import Visitor, syms, first_child_is_arith, ensure_visible +from black.nodes import Visitor, syms, is_arith_like, ensure_visible from black.nodes import is_docstring, is_empty_tuple, is_one_tuple, is_one_tuple_between from black.nodes import is_name_token, is_lpar_token, is_rpar_token from black.nodes import is_walrus_assignment, is_yield, is_vararg, is_multiline_string @@ -156,8 +156,12 @@ def visit_suite(self, node: Node) -> Iterator[Line]: def visit_simple_stmt(self, node: Node) -> Iterator[Line]: """Visit a statement without nested statements.""" - if first_child_is_arith(node): - wrap_in_parentheses(node, node.children[0], visible=False) + prev_type: Optional[int] = None + for child in node.children: + if (prev_type is None or prev_type == token.SEMI) and is_arith_like(child): + wrap_in_parentheses(node, child, visible=False) + prev_type = child.type + is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: if self.mode.is_pyi and is_stub_body(node): diff --git a/src/black/nodes.py b/src/black/nodes.py index 51d4cb8618d..7466670be5a 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -531,15 +531,14 @@ def first_leaf_column(node: Node) -> Optional[int]: return None -def first_child_is_arith(node: Node) -> bool: - """Whether first child is an arithmetic or a binary arithmetic expression""" - expr_types = { +def is_arith_like(node: LN) -> bool: + """Whether node is an arithmetic or a binary arithmetic expression""" + return node.type in { syms.arith_expr, syms.shift_expr, syms.xor_expr, syms.and_expr, } - return bool(node.children and node.children[0].type in expr_types) def is_docstring(leaf: Leaf) -> bool: From a24e1f795975350f7b1d8898d831916a9f6dbc6a Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Fri, 28 Jan 2022 18:13:18 -0800 Subject: [PATCH 29/50] Fix instability due to trailing comma logic (#2572) It was causing stability issues because the first pass could cause a "magic trailing comma" to appear, meaning that the second pass might get a different result. It's not critical. Some things format differently (with extra parens) --- CHANGES.md | 1 + src/black/__init__.py | 6 +-- src/black/linegen.py | 2 +- src/black/lines.py | 14 +---- src/black/nodes.py | 23 -------- tests/data/function_trailing_comma.py | 23 ++++---- tests/data/long_strings_flag_disabled.py | 13 +++-- tests/data/torture.py | 25 ++++----- tests/data/trailing_comma_optional_parens1.py | 54 ++++++++++--------- tests/data/trailing_comma_optional_parens2.py | 15 ++---- tests/data/trailing_comma_optional_parens3.py | 5 +- 11 files changed, 72 insertions(+), 109 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b57a360f1bc..6775cee14e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -139,6 +139,7 @@ and the first release covered by our new stability policy. when `--target-version py310` is explicitly specified (#2586) - Add support for parenthesized with (#2586) - Declare support for Python 3.10 for running Black (#2562) +- Fix unstable black runs around magic trailing comma (#2572) ### Integrations diff --git a/src/black/__init__.py b/src/black/__init__.py index 769e693ed23..6192f5c0f8e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1332,7 +1332,7 @@ def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]: return imports -def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: +def assert_equivalent(src: str, dst: str) -> None: """Raise AssertionError if `src` and `dst` aren't equivalent.""" try: src_ast = parse_ast(src) @@ -1349,7 +1349,7 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: except Exception as exc: log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst) raise AssertionError( - f"INTERNAL ERROR: Black produced invalid code on pass {pass_num}: {exc}. " + f"INTERNAL ERROR: Black produced invalid code: {exc}. " "Please report a bug on https://github.com/psf/black/issues. " f"This invalid output might be helpful: {log}" ) from None @@ -1360,7 +1360,7 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst")) raise AssertionError( "INTERNAL ERROR: Black produced code that is not equivalent to the" - f" source on pass {pass_num}. Please report a bug on " + f" source. Please report a bug on " f"https://github.com/psf/black/issues. This diff might be helpful: {log}" ) from None diff --git a/src/black/linegen.py b/src/black/linegen.py index 495d3230f8f..4dc242a1dfe 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -543,7 +543,7 @@ def right_hand_split( # there are no standalone comments in the body and not body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(body, line_length, omit_on_explode=omit) + and can_omit_invisible_parens(body, line_length) ): omit = {id(closing_bracket), *omit} try: diff --git a/src/black/lines.py b/src/black/lines.py index 1c4e38a96c1..f35665c8e0c 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -3,7 +3,6 @@ import sys from typing import ( Callable, - Collection, Dict, Iterator, List, @@ -22,7 +21,7 @@ from black.nodes import STANDALONE_COMMENT, TEST_DESCENDANTS from black.nodes import BRACKETS, OPENING_BRACKETS, CLOSING_BRACKETS from black.nodes import syms, whitespace, replace_child, child_towards -from black.nodes import is_multiline_string, is_import, is_type_comment, last_two_except +from black.nodes import is_multiline_string, is_import, is_type_comment from black.nodes import is_one_tuple_between # types @@ -645,7 +644,6 @@ def can_be_split(line: Line) -> bool: def can_omit_invisible_parens( line: Line, line_length: int, - omit_on_explode: Collection[LeafID] = (), ) -> bool: """Does `line` have a shape safe to reformat without optional parens around it? @@ -683,12 +681,6 @@ def can_omit_invisible_parens( penultimate = line.leaves[-2] last = line.leaves[-1] - if line.magic_trailing_comma: - try: - penultimate, last = last_two_except(line.leaves, omit=omit_on_explode) - except LookupError: - # Turns out we'd omit everything. We cannot skip the optional parentheses. - return False if ( last.type == token.RPAR @@ -710,10 +702,6 @@ def can_omit_invisible_parens( # unnecessary. return True - if line.magic_trailing_comma and penultimate.type == token.COMMA: - # The rightmost non-omitted bracket pair is the one we want to explode on. - return True - if _can_omit_closing_paren(line, last=last, line_length=line_length): return True diff --git a/src/black/nodes.py b/src/black/nodes.py index 7466670be5a..f130bff990e 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -4,13 +4,11 @@ import sys from typing import ( - Collection, Generic, Iterator, List, Optional, Set, - Tuple, TypeVar, Union, ) @@ -439,27 +437,6 @@ def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> b return prev_siblings_are(node.prev_sibling, tokens[:-1]) -def last_two_except(leaves: List[Leaf], omit: Collection[LeafID]) -> Tuple[Leaf, Leaf]: - """Return (penultimate, last) leaves skipping brackets in `omit` and contents.""" - stop_after: Optional[Leaf] = None - last: Optional[Leaf] = None - for leaf in reversed(leaves): - if stop_after: - if leaf is stop_after: - stop_after = None - continue - - if last: - return leaf, last - - if id(leaf) in omit: - stop_after = leaf.opening_bracket - else: - last = leaf - else: - raise LookupError("Last two leaves were also skipped") - - def parent_type(node: Optional[LN]) -> Optional[NodeType]: """ Returns: diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index 02078219e82..429eb0e330f 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -89,16 +89,19 @@ def f( "a": 1, "b": 2, }["a"] - if a == { - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - }["a"]: + if ( + a + == { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + }["a"] + ): pass diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/long_strings_flag_disabled.py index ef3094fd779..db3954e3abd 100644 --- a/tests/data/long_strings_flag_disabled.py +++ b/tests/data/long_strings_flag_disabled.py @@ -133,11 +133,14 @@ "Use f-strings instead!", ) -old_fmt_string3 = "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" % ( - "really really really really really", - "old", - "way to format strings!", - "Use f-strings instead!", +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) ) fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." diff --git a/tests/data/torture.py b/tests/data/torture.py index 79a44c2e34c..7cabd4c163f 100644 --- a/tests/data/torture.py +++ b/tests/data/torture.py @@ -31,20 +31,17 @@ def test(self, othr): ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 ) # -assert ( - sort_by_dependency( - { - "1": {"2", "3"}, - "2": {"2a", "2b"}, - "3": {"3a", "3b"}, - "2a": set(), - "2b": set(), - "3a": set(), - "3b": set(), - } - ) - == ["2a", "2b", "2", "3a", "3b", "3", "1"] -) +assert sort_by_dependency( + { + "1": {"2", "3"}, + "2": {"2a", "2b"}, + "3": {"3a", "3b"}, + "2a": set(), + "2b": set(), + "3a": set(), + "3b": set(), + } +) == ["2a", "2b", "2", "3a", "3b", "3", "1"] importA 0 diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/trailing_comma_optional_parens1.py index f9f4ae5e023..85aa8badb26 100644 --- a/tests/data/trailing_comma_optional_parens1.py +++ b/tests/data/trailing_comma_optional_parens1.py @@ -2,6 +2,10 @@ _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass +if x: + if y: + new_id = max(Vegetable.objects.order_by('-id')[0].id, + Mineral.objects.order_by('-id')[0].id) + 1 class X: def get_help_text(self): @@ -23,39 +27,37 @@ def b(self): # output -if ( - e1234123412341234.winerror - not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, - ) - or _check_timeout(t) -): +if e1234123412341234.winerror not in ( + _winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY, +) or _check_timeout(t): pass +if x: + if y: + new_id = ( + max( + Vegetable.objects.order_by("-id")[0].id, + Mineral.objects.order_by("-id")[0].id, + ) + + 1 + ) + class X: def get_help_text(self): - return ( - ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) - % {"min_length": self.min_length} - ) + return ngettext( + "Your password must contain at least %(min_length)d character.", + "Your password must contain at least %(min_length)d characters.", + self.min_length, + ) % {"min_length": self.min_length} class A: def b(self): - if ( - self.connection.mysql_is_mariadb - and ( - 10, - 4, - 3, - ) - < self.connection.mysql_version - < (10, 5, 2) - ): + if self.connection.mysql_is_mariadb and ( + 10, + 4, + 3, + ) < self.connection.mysql_version < (10, 5, 2): pass diff --git a/tests/data/trailing_comma_optional_parens2.py b/tests/data/trailing_comma_optional_parens2.py index 1dfb54ca687..9541670e394 100644 --- a/tests/data/trailing_comma_optional_parens2.py +++ b/tests/data/trailing_comma_optional_parens2.py @@ -4,14 +4,9 @@ # output -if ( - e123456.get_tk_patchlevel() >= (8, 6, 0, "final") - or ( - 8, - 5, - 8, - ) - <= get_tk_patchlevel() - < (8, 6) -): +if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( + 8, + 5, + 8, +) <= get_tk_patchlevel() < (8, 6): pass diff --git a/tests/data/trailing_comma_optional_parens3.py b/tests/data/trailing_comma_optional_parens3.py index bccf47430a7..c0ed699e6a6 100644 --- a/tests/data/trailing_comma_optional_parens3.py +++ b/tests/data/trailing_comma_optional_parens3.py @@ -18,7 +18,4 @@ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % { - "reported_username": reported_username, - "report_reason": report_reason, - } \ No newline at end of file + ) % {"reported_username": reported_username, "report_reason": report_reason} From a4992b4d50d6efa41b49ed0f804c5ed3723399db Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 28 Jan 2022 19:38:50 -0800 Subject: [PATCH 30/50] Add a test case to torture.py (#2822) Co-authored-by: hauntsaninja <> --- tests/data/torture.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/data/torture.py b/tests/data/torture.py index 7cabd4c163f..2a194759a82 100644 --- a/tests/data/torture.py +++ b/tests/data/torture.py @@ -22,6 +22,12 @@ def test(self, othr): (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + +assert ( + a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more) + == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} +) + # output importA @@ -76,3 +82,10 @@ def test(self, othr): othr.meta_data, othr.schedule, ) + + +assert a_function( + very_long_arguments_that_surpass_the_limit, + which_is_eighty_eight_in_this_case_plus_a_bit_more, +) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} + From 8acb8548c36882a124127d25287f4f38de3c2ff8 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 29 Jan 2022 10:37:51 -0500 Subject: [PATCH 31/50] Update classifiers to reflect stable (#2823) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5917998da4..8f904d2cc99 100644 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ def find_python_files(base: Path) -> List[Path]: }, test_suite="tests.test_black", classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", From 0d768e58f42d9aec20637d21ad261f7f9eaacae8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Jan 2022 08:00:59 -0800 Subject: [PATCH 32/50] Remove test suite from setup.py (#2824) We no longer use it --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 8f904d2cc99..466f1a9c3a6 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,6 @@ def find_python_files(base: Path) -> List[Path]: "uvloop": ["uvloop>=0.15.2"], "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"], }, - test_suite="tests.test_black", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", From c5f8e8bd5904ed21742b28afd7b1d84782a6a6e9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Jan 2022 09:32:26 -0800 Subject: [PATCH 33/50] Fix changelog entries in the wrong release (#2825) --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6775cee14e8..5e02027841b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ and the first release covered by our new stability policy. literals (#2799) - Treat blank lines in stubs the same inside top-level `if` statements (#2820) - Fix unstable formatting with semicolons and arithmetic expressions (#2817) +- Fix unstable formatting around magic trailing comma (#2572) ### Parser @@ -39,6 +40,7 @@ and the first release covered by our new stability policy. `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) - Fix handling of standalone `match()` or `case()` when there is a trailing newline or a comment inside of the parentheses. (#2760) +- `from __future__ import annotations` statement now implies Python 3.7+ (#2690) ### Performance @@ -95,7 +97,6 @@ and the first release covered by our new stability policy. - Fix determination of f-string expression spans (#2654) - Fix bad formatting of error messages about EOF in multi-line statements (#2343) - Functions and classes in blocks now have more consistent surrounding spacing (#2472) -- `from __future__ import annotations` statement now implies Python 3.7+ (#2690) #### Jupyter Notebook support @@ -139,7 +140,6 @@ and the first release covered by our new stability policy. when `--target-version py310` is explicitly specified (#2586) - Add support for parenthesized with (#2586) - Declare support for Python 3.10 for running Black (#2562) -- Fix unstable black runs around magic trailing comma (#2572) ### Integrations From dea2f94ebd33081bdf8fa75611424890fcb3cace Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Jan 2022 09:32:52 -0800 Subject: [PATCH 34/50] Fix changelog entries in the wrong release (#2825) From d038a24ca200da9dacc1dcb05090c9e5b45b7869 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 29 Jan 2022 14:30:25 -0500 Subject: [PATCH 35/50] Prepare docs for release 22.1.0 (GH-2826) --- CHANGES.md | 2 +- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5e02027841b..9c92f8f9b58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 22.1.0 At long last, _Black_ is no longer a beta product! This is the first non-beta release and the first release covered by our new stability policy. diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 9c53f30687d..7215e111f5c 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -7,7 +7,7 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black # It is recommended to specify the latest version of Python diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index b82cef4a52d..48dda3ba036 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -173,7 +173,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, version 21.12b0 +black, version 22.1.0 ``` An option to require a specific version to be running is also provided. From bbe1bdf1edfedf51b40824c5574413c0b1b35284 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 30 Jan 2022 11:53:45 -0800 Subject: [PATCH 36/50] Adjust `--preview` documentation (#2833) --- src/black/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6192f5c0f8e..6a703e45046 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -258,7 +258,7 @@ def validate_regex( "--preview", is_flag=True, help=( - "Enable potentially disruptive style changes that will be added to Black's main" + "Enable potentially disruptive style changes that may be added to Black's main" " functionality in the next major release." ), ) From f61299a62a330dd26d180a8ea420916870f19730 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 30 Jan 2022 12:01:56 -0800 Subject: [PATCH 37/50] Exclude __pypackages__ by default (GH-2836) PDM uses this as part of not-accepted-yet PEP 582. --- CHANGES.md | 6 ++++++ src/black/const.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9c92f8f9b58..a840e013041 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### Configuration + +- Do not format `__pypackages__` directories by default (#2836) + ## 22.1.0 At long last, _Black_ is no longer a beta product! This is the first non-beta release diff --git a/src/black/const.py b/src/black/const.py index dbb4826be0e..03afc96e8d6 100644 --- a/src/black/const.py +++ b/src/black/const.py @@ -1,4 +1,4 @@ DEFAULT_LINE_LENGTH = 88 -DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 +DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_build|buck-out|build|dist|__pypackages__)/" # noqa: B950 DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$" STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" From cae7ae3a4d32dc51e0752d4a4e885a7792a0286d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9rik=20Paradis?= Date: Sun, 30 Jan 2022 16:42:56 -0500 Subject: [PATCH 38/50] Soft comparison of --required-version (#2832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jelle Zijlstra Co-authored-by: Felix Hildén --- CHANGES.md | 1 + src/black/__init__.py | 9 +++++++-- tests/test_black.py | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a840e013041..7d74e56ce4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### Configuration - Do not format `__pypackages__` directories by default (#2836) +- Add support for specifying stable version with `--required-version` (#2832). ## 22.1.0 diff --git a/src/black/__init__.py b/src/black/__init__.py index 6a703e45046..8c28b6ba18b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -291,7 +291,8 @@ def validate_regex( type=str, help=( "Require a specific version of Black to be running (useful for unifying results" - " across many environments e.g. with a pyproject.toml file)." + " across many environments e.g. with a pyproject.toml file). It can be" + " either a major version number or an exact version." ), ) @click.option( @@ -474,7 +475,11 @@ def main( out(f"Using configuration in '{config}'.", fg="blue") error_msg = "Oh no! 💥 💔 💥" - if required_version and required_version != __version__: + if ( + required_version + and required_version != __version__ + and required_version != __version__.split(".")[0] + ): err( f"{error_msg} The required version `{required_version}` does not match" f" the running version `{__version__}`!" diff --git a/tests/test_black.py b/tests/test_black.py index 2dd284f2cd6..b04c0a66fe9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1198,6 +1198,20 @@ def test_required_version_matches_version(self) -> None: ignore_config=True, ) + def test_required_version_matches_partial_version(self) -> None: + self.invokeBlack( + ["--required-version", black.__version__.split(".")[0], "-c", "0"], + exit_code=0, + ignore_config=True, + ) + + def test_required_version_does_not_match_on_minor_version(self) -> None: + self.invokeBlack( + ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"], + exit_code=1, + ignore_config=True, + ) + def test_required_version_does_not_match_version(self) -> None: result = BlackRunner().invoke( black.main, From afc0fb05cbb1f7ea2700a7e5d240079df00f6d07 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 30 Jan 2022 14:04:06 -0800 Subject: [PATCH 39/50] release process: formalize the changelog template (#2837) I did this manually for the last few releases and I think it's going to be helpful in the future too. Unfortunately this adds a little more work during the release (sorry @cooperlees). This change will also improve the merge conflict situation a bit, because changes to different sections won't merge conflict. For the last release, the sections were in a kind of random order. In the template I put highlights and "Style" first because they're most important to users, and alphabetized the rest. --- CHANGES.md | 39 +++++++++++++++++++ docs/contributing/release_process.md | 56 +++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7d74e56ce4e..ba693241c19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,11 +2,50 @@ ## Unreleased +### Highlights + + + +### Style + + + +### _Blackd_ + + + ### Configuration + + - Do not format `__pypackages__` directories by default (#2836) - Add support for specifying stable version with `--required-version` (#2832). +### Documentation + + + +### Integrations + + + +### Output + + + +### Packaging + + + +### Parser + + + +### Performance + + + ## 22.1.0 At long last, _Black_ is no longer a beta product! This is the first non-beta release diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 9ee7dbc607c..89beb099e66 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -9,8 +9,10 @@ To cut a release, you must be a _Black_ maintainer with `GitHub Release` creatio access. Using this access, the release process is: 1. Cut a new PR editing `CHANGES.md` and the docs to version the latest changes - 1. Example PR: [#2616](https://github.com/psf/black/pull/2616) - 2. Example title: `Update CHANGES.md for XX.X release` + 1. Remove any empty sections for the current release + 2. Add a new empty template for the next release (template below) + 3. Example PR: [#2616](https://github.com/psf/black/pull/2616) + 4. Example title: `Update CHANGES.md for XX.X release` 2. Once the release PR is merged ensure all CI passes 1. If not, ensure there is an Issue open for the cause of failing CI (generally we'd want this fixed before cutting a release) @@ -32,6 +34,56 @@ access. Using this access, the release process is: If anything fails, please go read the respective action's log output and configuration file to reverse engineer your way to a fix/soluton. +## Changelog template + +Use the following template for a clean changelog after the release: + +``` +## Unreleased + +### Highlights + + + +### Style + + + +### _Blackd_ + + + +### Configuration + + + +### Documentation + + + +### Integrations + + + +### Output + + + +### Packaging + + + +### Parser + + + +### Performance + + + +``` + ## Release workflows All _Blacks_'s automation workflows use GitHub Actions. All workflows are therefore From f3f3acc4440543cd7b8bf7cb4d4cea7300a251ef Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Mon, 31 Jan 2022 19:06:52 -0500 Subject: [PATCH 40/50] Surface links to Stability Policy (GH-2848) --- CHANGES.md | 3 ++- README.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ba693241c19..4ad9e532808 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -49,7 +49,8 @@ ## 22.1.0 At long last, _Black_ is no longer a beta product! This is the first non-beta release -and the first release covered by our new stability policy. +and the first release covered by our new +[stability policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy). ### Highlights diff --git a/README.md b/README.md index a00495c8858..eda07b18a68 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ also documented. They're both worth taking a look: - [The _Black_ Code Style: Current style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) - [The _Black_ Code Style: Future style](https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html) +Changes to the _Black_ code style are bound by the Stability Policy: + +- [The _Black_ Code Style: Stability Policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy) + Please refer to this document before submitting an issue. What seems like a bug might be intended behaviour. From fb9fe6b565ce8a9beeebb51c23f384d1865d0ee8 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 1 Feb 2022 00:29:01 -0500 Subject: [PATCH 41/50] Isolate command line tests from user-level config (#2851) --- tests/test_black.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index b04c0a66fe9..cd38d9e2c0d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -63,6 +63,7 @@ ) THIS_FILE = Path(__file__) +EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml" PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS] DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES) DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES) @@ -159,7 +160,12 @@ def test_piping(self) -> None: source, expected = read_data("src/black/__init__", data=False) result = BlackRunner().invoke( black.main, - ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"], + [ + "-", + "--fast", + f"--line-length={black.DEFAULT_LINE_LENGTH}", + f"--config={EMPTY_CONFIG}", + ], input=BytesIO(source.encode("utf8")), ) self.assertEqual(result.exit_code, 0) @@ -175,13 +181,12 @@ def test_piping_diff(self) -> None: ) source, _ = read_data("expression.py") expected, _ = read_data("expression.diff") - config = THIS_DIR / "data" / "empty_pyproject.toml" args = [ "-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}", "--diff", - f"--config={config}", + f"--config={EMPTY_CONFIG}", ] result = BlackRunner().invoke( black.main, args, input=BytesIO(source.encode("utf8")) @@ -193,14 +198,13 @@ def test_piping_diff(self) -> None: def test_piping_diff_with_color(self) -> None: source, _ = read_data("expression.py") - config = THIS_DIR / "data" / "empty_pyproject.toml" args = [ "-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}", "--diff", "--color", - f"--config={config}", + f"--config={EMPTY_CONFIG}", ] result = BlackRunner().invoke( black.main, args, input=BytesIO(source.encode("utf8")) @@ -252,7 +256,6 @@ def test_expression_ff(self) -> None: def test_expression_diff(self) -> None: source, _ = read_data("expression.py") - config = THIS_DIR / "data" / "empty_pyproject.toml" expected, _ = read_data("expression.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( @@ -261,7 +264,7 @@ def test_expression_diff(self) -> None: ) try: result = BlackRunner().invoke( - black.main, ["--diff", str(tmp_file), f"--config={config}"] + black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"] ) self.assertEqual(result.exit_code, 0) finally: @@ -279,12 +282,12 @@ def test_expression_diff(self) -> None: def test_expression_diff_with_color(self) -> None: source, _ = read_data("expression.py") - config = THIS_DIR / "data" / "empty_pyproject.toml" expected, _ = read_data("expression.diff") tmp_file = Path(black.dump_to_file(source)) try: result = BlackRunner().invoke( - black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"] + black.main, + ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"], ) finally: os.unlink(tmp_file) @@ -325,7 +328,9 @@ def test_skip_magic_trailing_comma(self) -> None: r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" ) try: - result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)]) + result = BlackRunner().invoke( + black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"] + ) self.assertEqual(result.exit_code, 0) finally: os.unlink(tmp_file) From 111880efc7938c618dd16c7cf8d872ca32c6a751 Mon Sep 17 00:00:00 2001 From: Peter Mescalchin Date: Wed, 2 Feb 2022 14:17:45 +1100 Subject: [PATCH 42/50] Update description for GitHub Action `options:` argument (GH-2858) It was missing --diff as one of the default arguments passed. --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index dd2de1b62ad..dbd8ef69ec2 100644 --- a/action.yml +++ b/action.yml @@ -5,7 +5,7 @@ inputs: options: description: "Options passed to Black. Use `black --help` to see available options. Default: - '--check'" + '--check --diff'" required: false default: "--check --diff" src: From 31fe97e7ce1055debaa54bed9c63e252508a9a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Wed, 2 Feb 2022 08:59:42 +0200 Subject: [PATCH 43/50] Create indentation FAQ entry (#2855) Co-authored-by: Jelle Zijlstra --- docs/faq.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 264141e3f39..70f9b51394f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,6 +8,18 @@ The most common questions and issues users face are aggregated to this FAQ. :class: this-will-duplicate-information-and-it-is-still-useful-here ``` +## Why spaces? I prefer tabs + +PEP 8 recommends spaces over tabs, and they are used by most of the Python community. +_Black_ provides no options to configure the indentation style, and requests for such +options will not be considered. + +However, we recognise that using tabs is an accessibility issue as well. While the +option will never be added to _Black_, visually impaired developers may find conversion +tools such as `expand/unexpand` (for Linux) useful when contributing to Python projects. +A workflow might consist of e.g. setting up appropriate pre-commit and post-merge git +hooks, and scripting `unexpand` to run after applying _Black_. + ## Does Black have an API? Not yet. _Black_ is fundamentally a command line tool. Many From 01001d5cff788c2aed17c5f0379d3ef37b95825d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 07:31:58 -0800 Subject: [PATCH 44/50] Bump sphinx-copybutton from 0.4.0 to 0.5.0 in /docs (#2871) Bumps [sphinx-copybutton](https://github.com/executablebooks/sphinx-copybutton) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/executablebooks/sphinx-copybutton/releases) - [Changelog](https://github.com/executablebooks/sphinx-copybutton/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/sphinx-copybutton/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: sphinx-copybutton dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 01fea693f07..0b685425dde 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,5 +3,5 @@ myst-parser==0.16.1 Sphinx==4.4.0 sphinxcontrib-programoutput==0.17 -sphinx_copybutton==0.4.0 +sphinx_copybutton==0.5.0 furo==2022.1.2 From 9b317178d62f9397b7e792d0f6dda827693df1b3 Mon Sep 17 00:00:00 2001 From: Paolo Melchiorre Date: Tue, 8 Feb 2022 20:38:39 +0100 Subject: [PATCH 45/50] Add Django in 'used by' section in Readme (#2875) * Add Django in 'used by' section in Readme * Fix Readme issue --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eda07b18a68..8ba9d6ceb98 100644 --- a/README.md +++ b/README.md @@ -130,10 +130,10 @@ code in compliance with many other _Black_ formatted projects. ## Used by The following notable open-source projects trust _Black_ with enforcing a consistent -code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy, -Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, -Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, -OpenOA, FLORIS, ORBIT, WOMBAT, and many more. +code style: pytest, tox, Pyramid, Django, Django Channels, Hypothesis, attrs, +SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), +pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, +Zulip, Kedro, OpenOA, FLORIS, ORBIT, WOMBAT, and many more. The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Mozilla, Quora, Duolingo, QuantumBlack, Tesla. From b4a6bb08fa704facbf3397f95b3216e13c3c964a Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 8 Feb 2022 21:13:58 +0100 Subject: [PATCH 46/50] Avoid crashing when the user has no homedir (#2814) --- CHANGES.md | 1 + src/black/files.py | 6 +++++- tests/test_black.py | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4ad9e532808..e94b345e92a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ - Do not format `__pypackages__` directories by default (#2836) - Add support for specifying stable version with `--required-version` (#2832). +- Avoid crashing when the user has no homedir (#2814) ### Documentation diff --git a/src/black/files.py b/src/black/files.py index 18c84237bf0..8348e0d8c28 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -87,7 +87,7 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: if path_user_pyproject_toml.is_file() else None ) - except PermissionError as e: + except (PermissionError, RuntimeError) as e: # We do not have access to the user-level config directory, so ignore it. err(f"Ignoring user configuration directory due to {e!r}") return None @@ -111,6 +111,10 @@ def find_user_pyproject_toml() -> Path: This looks for ~\.black on Windows and ~/.config/black on Linux and other Unix systems. + + May raise: + - RuntimeError: if the current user has no homedir + - PermissionError: if the current process cannot access the user's homedir """ if sys.platform == "win32": # Windows diff --git a/tests/test_black.py b/tests/test_black.py index cd38d9e2c0d..82abd47dffd 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -10,7 +10,7 @@ import types import unittest from concurrent.futures import ThreadPoolExecutor -from contextlib import contextmanager +from contextlib import contextmanager, redirect_stderr from dataclasses import replace from io import BytesIO from pathlib import Path @@ -1358,6 +1358,21 @@ def test_find_project_root(self) -> None: (src_dir.resolve(), "pyproject.toml"), ) + @patch( + "black.files.find_user_pyproject_toml", + ) + def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None: + find_user_pyproject_toml.side_effect = RuntimeError() + + with redirect_stderr(io.StringIO()) as stderr: + result = black.files.find_pyproject_toml( + path_search_start=(str(Path.cwd().root),) + ) + + assert result is None + err = stderr.getvalue() + assert "Ignoring user configuration" in err + @patch( "black.files.find_user_pyproject_toml", black.files.find_user_pyproject_toml.__wrapped__, From 862c6f2c0c99b34731bd1e8812297fd2803e6a8b Mon Sep 17 00:00:00 2001 From: "Xuan (Sean) Hu" Date: Fri, 11 Feb 2022 09:31:28 +0800 Subject: [PATCH 47/50] Order the disabled error codes for pylint (GH-2870) Just make them alphabetical. --- docs/guides/using_black_with_other_tools.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 9938d814073..bde99f7c00c 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -210,7 +210,7 @@ mixed feelings about _Black_'s formatting style. #### Configuration ``` -disable = C0330, C0326 +disable = C0326, C0330 max-line-length = 88 ``` @@ -243,7 +243,7 @@ characters via `max-line-length = 88`. ```ini [MESSAGES CONTROL] -disable = C0330, C0326 +disable = C0326, C0330 [format] max-line-length = 88 @@ -259,7 +259,7 @@ max-line-length = 88 max-line-length = 88 [pylint.messages_control] -disable = C0330, C0326 +disable = C0326, C0330 ``` @@ -269,7 +269,7 @@ disable = C0330, C0326 ```toml [tool.pylint.messages_control] -disable = "C0330, C0326" +disable = "C0326, C0330" [tool.pylint.format] max-line-length = "88" From 07a2e6f67810a8949b76a26c434c91d3fda7ac24 Mon Sep 17 00:00:00 2001 From: Laurent Lyaudet Date: Fri, 11 Feb 2022 02:32:55 +0100 Subject: [PATCH 48/50] Fix typo in file_collection_and_discovery.md (GH-2860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "you your" -> "your" Co-authored-by: Felix Hildén --- docs/usage_and_configuration/file_collection_and_discovery.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md index bd90ccc6af8..de1d5e6c11e 100644 --- a/docs/usage_and_configuration/file_collection_and_discovery.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -24,8 +24,8 @@ as .pyi, and whether string normalization was omitted. To override the location of these files on all systems, set the environment variable `BLACK_CACHE_DIR` to the preferred location. Alternatively on macOS and Linux, set -`XDG_CACHE_HOME` to you your preferred location. For example, if you want to put the -cache in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`. +`XDG_CACHE_HOME` to your preferred location. For example, if you want to put the cache +in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`. _Black_ will then write the above files to `.cache/black`. Note that `BLACK_CACHE_DIR` will take precedence over `XDG_CACHE_HOME` if both are set. From 50a856970d2453087662a295631d6f24a12bc3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9rik=20Paradis?= Date: Sun, 20 Feb 2022 20:17:01 -0500 Subject: [PATCH 49/50] Isolate command line tests for notebooks from user-level config (#2854) --- tests/test_ipynb.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index d78a68cd9a0..473047a3b32 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -24,6 +24,8 @@ JUPYTER_MODE = Mode(is_ipynb=True) +EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml" + runner = CliRunner() @@ -410,6 +412,7 @@ def test_ipynb_diff_with_change() -> None: [ str(DATA_DIR / "notebook_trailing_newline.ipynb"), "--diff", + f"--config={EMPTY_CONFIG}", ], ) expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' @@ -422,6 +425,7 @@ def test_ipynb_diff_with_no_change() -> None: [ str(DATA_DIR / "notebook_without_changes.ipynb"), "--diff", + f"--config={EMPTY_CONFIG}", ], ) expected = "1 file would be left unchanged." @@ -440,13 +444,17 @@ def test_cache_isnt_written_if_no_jupyter_deps_single( monkeypatch.setattr( "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) - result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")]) + result = runner.invoke( + main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"] + ) assert "No Python files are present to be formatted. Nothing to do" in result.output jupyter_dependencies_are_installed.cache_clear() monkeypatch.setattr( "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True ) - result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")]) + result = runner.invoke( + main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"] + ) assert "reformatted" in result.output @@ -462,13 +470,13 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( monkeypatch.setattr( "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) - result = runner.invoke(main, [str(tmp_path)]) + result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"]) assert "No Python files are present to be formatted. Nothing to do" in result.output jupyter_dependencies_are_installed.cache_clear() monkeypatch.setattr( "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True ) - result = runner.invoke(main, [str(tmp_path)]) + result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"]) assert "reformatted" in result.output @@ -483,6 +491,7 @@ def test_ipynb_flag(tmp_path: pathlib.Path) -> None: str(tmp_nb), "--diff", "--ipynb", + f"--config={EMPTY_CONFIG}", ], ) expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' @@ -498,6 +507,7 @@ def test_ipynb_and_pyi_flags() -> None: "--pyi", "--ipynb", "--diff", + f"--config={EMPTY_CONFIG}", ], ) assert isinstance(result.exception, SystemExit) From 8089aaad6b0116eb3a4758430129c3d8d900585b Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Sun, 20 Feb 2022 20:37:07 -0500 Subject: [PATCH 50/50] correct Vim integration code (#2853) - use `Black` directly: the commands an autocommand runs are Ex commands, so no execute or colon is necessary. - use an `augroup` (best practice) to prevent duplicate autocommands from hindering performance. --- docs/integrations/editors.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 5d2f83ace8a..1c7879b63a6 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -189,10 +189,13 @@ If you need to do anything special to make your virtualenv work and install _Bla example you want to run a version from main), create a virtualenv manually and point `g:black_virtualenv` to it. The plugin will use it. -To run _Black_ on save, add the following line to `.vimrc` or `init.vim`: +To run _Black_ on save, add the following lines to `.vimrc` or `init.vim`: ``` -autocmd BufWritePre *.py execute ':Black' +augroup black_on_save + autocmd! + autocmd BufWritePre *.py Black +augroup end ``` To run _Black_ on a key press (e.g. F9 below), add this: