From 22747a6937d53e38397e96c4ed5ed0571db31f71 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Sat, 18 Sep 2021 22:18:06 +0100 Subject: [PATCH 001/700] fix all b904s (#2501) --- src/black/__init__.py | 19 ++++++++++--------- src/black/linegen.py | 10 +++++----- src/blackd/__init__.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index d033e01141a..7fed1355f47 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -114,7 +114,7 @@ def read_pyproject_toml( except (OSError, ValueError) as e: raise click.FileError( filename=value, hint=f"Error reading configuration file: {e}" - ) + ) from None if not config: return None @@ -172,7 +172,7 @@ def validate_regex( try: return re_compile_maybe_verbose(value) if value is not None else None except re.error: - raise click.BadParameter("Not a valid regular expression") + raise click.BadParameter("Not a valid regular expression") from None @click.command(context_settings=dict(help_option_names=["-h", "--help"])) @@ -777,7 +777,9 @@ def format_file_in_place( except NothingChanged: return False except JSONDecodeError: - raise ValueError(f"File '{src}' cannot be parsed as valid Jupyter notebook.") + raise ValueError( + f"File '{src}' cannot be parsed as valid Jupyter notebook." + ) from None if write_back == WriteBack.YES: with open(src, "w", encoding=encoding, newline=newline) as f: @@ -947,7 +949,7 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str: try: masked_src, replacements = mask_cell(src_without_trailing_semicolon) except SyntaxError: - raise NothingChanged + raise NothingChanged from None masked_dst = format_str(masked_src, mode=mode) if not fast: check_stability_and_equivalence(masked_src, masked_dst, mode=mode) @@ -957,7 +959,7 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str: ) dst = dst.rstrip("\n") if dst == src: - raise NothingChanged + raise NothingChanged from None return dst @@ -970,7 +972,7 @@ def validate_metadata(nb: MutableMapping[str, Any]) -> None: """ language = nb.get("metadata", {}).get("language_info", {}).get("name", None) if language is not None and language != "python": - raise NothingChanged + raise NothingChanged from None def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: @@ -1202,9 +1204,8 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: src_ast = parse_ast(src) except Exception as exc: raise AssertionError( - "cannot use --safe with this file; failed to parse source file. AST" - f" error message: {exc}" - ) + "cannot use --safe with this file; failed to parse source file." + ) from exc try: dst_ast = parse_ast(dst) diff --git a/src/black/linegen.py b/src/black/linegen.py index fafaf1032ca..eb53fa0ac56 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -503,14 +503,14 @@ def right_hand_split( yield from right_hand_split(line, line_length, features=features, omit=omit) return - except CannotSplit: + except CannotSplit as e: if not ( can_be_split(body) or is_line_short_enough(body, line_length=line_length) ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." - ) + ) from e elif head.contains_multiline_strings() or tail.contains_multiline_strings(): raise CannotSplit( @@ -518,7 +518,7 @@ def right_hand_split( " satisfy the splitting algorithm because the head or the tail" " contains multiline strings which by definition never fit one" " line." - ) + ) from e ensure_visible(opening_bracket) ensure_visible(closing_bracket) @@ -635,13 +635,13 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[ try: last_leaf = line.leaves[-1] except IndexError: - raise CannotSplit("Line empty") + raise CannotSplit("Line empty") from None bt = line.bracket_tracker try: delimiter_priority = bt.max_delimiter_priority(exclude={id(last_leaf)}) except ValueError: - raise CannotSplit("No delimiters found") + raise CannotSplit("No delimiters found") from None if delimiter_priority == DOT_PRIORITY: if bt.delimiter_count_with_priority(delimiter_priority) == 1: diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 5fdec152226..d062303fd10 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -195,7 +195,7 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi raise InvalidVariantHeader(f"3.{minor} is not supported") versions.add(black.TargetVersion[version_str]) except (KeyError, ValueError): - raise InvalidVariantHeader("expected e.g. '3.7', 'py3.5'") + raise InvalidVariantHeader("expected e.g. '3.7', 'py3.5'") from None return False, versions From 0540591e256c6121ee0bac970024501fcdcb8c0c Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sun, 19 Sep 2021 04:24:09 +0100 Subject: [PATCH 002/700] add check for version in the-basics example (#2459) --- .pre-commit-config.yaml | 13 +++++- scripts/check_version_in_basics_example.py | 47 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 scripts/check_version_in_basics_example.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3cbe352e38..a3cd6639384 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,18 @@ repos: entry: python -m scripts.check_pre_commit_rev_in_example files: '(CHANGES\.md|source_version_control\.md)$' additional_dependencies: - ["commonmark==0.9.1", "pyyaml==5.4.1", "beautifulsoup4==4.9.3"] + &version_check_dependencies [ + commonmark==0.9.1, + pyyaml==5.4.1, + beautifulsoup4==4.9.3, + ] + + - id: check-version-in-the-basics-example + name: Check black version in the basics example + language: python + entry: python -m scripts.check_version_in_basics_example + files: '(CHANGES\.md|the_basics\.md)$' + additional_dependencies: *version_check_dependencies - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 diff --git a/scripts/check_version_in_basics_example.py b/scripts/check_version_in_basics_example.py new file mode 100644 index 00000000000..c62780d97ab --- /dev/null +++ b/scripts/check_version_in_basics_example.py @@ -0,0 +1,47 @@ +""" +Check that the rev value in the example from ``the_basics.md`` matches +the latest version of Black. This saves us from forgetting to update that +during the release process. +""" + +import os +import sys + +import commonmark +from bs4 import BeautifulSoup + + +def main(changes: str, the_basics: str) -> None: + changes_html = commonmark.commonmark(changes) + changes_soup = BeautifulSoup(changes_html, "html.parser") + headers = changes_soup.find_all("h2") + tags = [header.string for header in headers if header.string != "Unreleased"] + latest_tag = tags[0] + + the_basics_html = commonmark.commonmark(the_basics) + the_basics_soup = BeautifulSoup(the_basics_html, "html.parser") + (version_example,) = [ + code_block.string + for code_block in the_basics_soup.find_all(class_="language-console") + if "$ black --version" in code_block.string + ] + + for tag in tags: + if tag in version_example and tag != latest_tag: + print( + "Please set the version in the ``black --version`` " + "example from ``the_basics.md`` to be the latest one.\n" + f"Expected {latest_tag}, got {tag}.\n" + ) + sys.exit(1) + + +if __name__ == "__main__": + with open("CHANGES.md", encoding="utf-8") as fd: + changes = fd.read() + with open( + os.path.join("docs", "usage_and_configuration", "the_basics.md"), + encoding="utf-8", + ) as fd: + the_basics = fd.read() + main(changes, the_basics) From 37861b4ce264f16754f4459d19522a05844daf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Sun, 19 Sep 2021 11:15:39 +0200 Subject: [PATCH 003/700] DOC: cleanup pre-commit instructions following #2430 (#2481) --- docs/integrations/source_version_control.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index b07796fda0b..6a1aa363d2b 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -10,7 +10,11 @@ repos: rev: 21.9b0 hooks: - id: black - language_version: python3 # Should be a command that runs python3.6+ + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.9 ``` Feel free to switch out the `rev` value to something else, like another From 7b153936587e17b9db992a2d8c8b6cfba3ef7209 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Thu, 23 Sep 2021 22:20:48 -0400 Subject: [PATCH 004/700] Add Kedro to project list and QuantumBlack to orgs (#2502) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 709478e1d58..7bf0ed8d16f 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,11 @@ 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, and many -more. +Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, and +many more. -The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo. +The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo, +QuantumBlack. Are we missing anyone? Let us know. From a5381ba7648f7308145c78c248e29118e18dc530 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Sat, 25 Sep 2021 12:58:44 +0100 Subject: [PATCH 005/700] re-implement simple CORS middleware for blackd (#2500) * re-implement simple CORS middleware for blackd * remove aiohttp-cors from setup.py * Remove aiohttp-cors from Pipfile.lock Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 6 ++++++ Pipfile | 1 - Pipfile.lock | 42 ++++++++++++++++++++++----------------- setup.py | 2 +- src/blackd/__init__.py | 19 +++++------------- src/blackd/middlewares.py | 34 +++++++++++++++++++++++++++++++ tests/test_blackd.py | 21 ++++++++++++++++++++ 7 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 src/blackd/middlewares.py diff --git a/CHANGES.md b/CHANGES.md index 6ff488ba74e..2a9b649d25c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Blackd_ + +- Remove dependency on aiohttp-cors (#2500) + ## 21.9b0 ### Packaging diff --git a/Pipfile b/Pipfile index 824beda16cc..66ded8b8570 100644 --- a/Pipfile +++ b/Pipfile @@ -41,7 +41,6 @@ black = {editable = true, extras = ["d", "jupyter"], path = "."} [packages] aiohttp = ">=3.6.0" -aiohttp-cors = ">=0.4.0" platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" diff --git a/Pipfile.lock b/Pipfile.lock index 30a4defcca3..0cbe9011981 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ebf216584cfb2c962a1792d0682f3c08b44c7ae27305a03a54eacd6f42df27db" + "sha256": "8b28e41c5a63f0c30361d2a0ed29dc1e3f0468223ef150ae68586839e2ccf1c9" }, "pipfile-spec": 6, "requires": {}, @@ -57,14 +57,6 @@ "index": "pypi", "version": "==3.7.4.post0" }, - "aiohttp-cors": { - "hashes": [ - "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", - "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d" - ], - "index": "pypi", - "version": "==0.7.0" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -188,6 +180,14 @@ "index": "pypi", "version": "==0.4.3" }, + "packaging": { + "hashes": [ + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, "pathspec": { "hashes": [ "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", @@ -204,6 +204,14 @@ "index": "pypi", "version": "==2.2.0" }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, "regex": { "hashes": [ "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd", @@ -252,6 +260,9 @@ "version": "==2021.8.21" }, "setuptools-scm": { + "extras": [ + "toml" + ], "hashes": [ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" @@ -410,14 +421,6 @@ "index": "pypi", "version": "==3.7.4.post0" }, - "aiohttp-cors": { - "hashes": [ - "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", - "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d" - ], - "index": "pypi", - "version": "==0.7.0" - }, "alabaster": { "hashes": [ "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", @@ -769,7 +772,7 @@ "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977", "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b" ], - "markers": "python_version < '3.7' and python_version < '3.7'", + "markers": "python_version < '3.7'", "version": "==5.2.2" }, "iniconfig": { @@ -1336,6 +1339,9 @@ "version": "==3.3.1" }, "setuptools-scm": { + "extras": [ + "toml" + ], "hashes": [ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" diff --git a/setup.py b/setup.py index 929096a2098..19df9beb24e 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def get_long_description() -> str: "mypy_extensions>=0.4.3", ], extras_require={ - "d": ["aiohttp>=3.6.0", "aiohttp-cors>=0.4.0"], + "d": ["aiohttp>=3.6.0"], "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], "uvloop": ["uvloop>=0.15.2"], diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index d062303fd10..cc966404a74 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -8,7 +8,7 @@ try: from aiohttp import web - import aiohttp_cors + from .middlewares import cors except ImportError as ie: raise ImportError( f"aiohttp dependency is not installed: {ie}. " @@ -67,20 +67,11 @@ def main(bind_host: str, bind_port: int) -> None: def make_app() -> web.Application: - app = web.Application() - executor = ProcessPoolExecutor() - - cors = aiohttp_cors.setup(app) - resource = cors.add(app.router.add_resource("/")) - cors.add( - resource.add_route("POST", partial(handle, executor=executor)), - { - "*": aiohttp_cors.ResourceOptions( - allow_headers=(*BLACK_HEADERS, "Content-Type"), expose_headers="*" - ) - }, + app = web.Application( + middlewares=[cors(allow_headers=(*BLACK_HEADERS, "Content-Type"))] ) - + executor = ProcessPoolExecutor() + app.add_routes([web.post("/", partial(handle, executor=executor))]) return app diff --git a/src/blackd/middlewares.py b/src/blackd/middlewares.py new file mode 100644 index 00000000000..97994ecc1df --- /dev/null +++ b/src/blackd/middlewares.py @@ -0,0 +1,34 @@ +from typing import Iterable, Awaitable, Callable +from aiohttp.web_response import StreamResponse +from aiohttp.web_request import Request +from aiohttp.web_middlewares import middleware + +Handler = Callable[[Request], Awaitable[StreamResponse]] +Middleware = Callable[[Request, Handler], Awaitable[StreamResponse]] + + +def cors(allow_headers: Iterable[str]) -> Middleware: + @middleware + async def impl(request: Request, handler: Handler) -> StreamResponse: + is_options = request.method == "OPTIONS" + is_preflight = is_options and "Access-Control-Request-Method" in request.headers + if is_preflight: + resp = StreamResponse() + else: + resp = await handler(request) + + origin = request.headers.get("Origin") + if not origin: + return resp + + resp.headers["Access-Control-Allow-Origin"] = "*" + resp.headers["Access-Control-Expose-Headers"] = "*" + if is_options: + resp.headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers) + resp.headers["Access-Control-Allow-Methods"] = ", ".join( + ("OPTIONS", "POST") + ) + + return resp + + return impl # type: ignore diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 9ca19d49dc6..cc750b40567 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -164,3 +164,24 @@ async def test_blackd_invalid_line_length(self) -> None: async def test_blackd_response_black_version_header(self) -> None: response = await self.client.post("/") self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) + + @unittest_run_loop + async def test_cors_preflight(self) -> None: + response = await self.client.options( + "/", + headers={ + "Access-Control-Request-Method": "POST", + "Origin": "*", + "Access-Control-Request-Headers": "Content-Type", + }, + ) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) + + @unittest_run_loop + async def test_cors_headers_present(self) -> None: + response = await self.client.post("/", headers={"Origin": "*"}) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) From 1af533108968e40cc005079a328ad117a864af13 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Sat, 25 Sep 2021 20:45:13 +0100 Subject: [PATCH 006/700] Bump required aiohttp version to 3.7.4 (#2509) Commit history before merge: * Bump required aiohttp version to 3.7.4 This release includes an important security fix (https://github.com/aio-libs/aiohttp/security/advisories/GHSA-v6wp-4m6f-gcjg) and many other improvements. * add changelog entry * Let's not forget about Pipfile Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + Pipfile | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2a9b649d25c..1ff2cea84ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Blackd_ - Remove dependency on aiohttp-cors (#2500) +- Bump required aiohttp version to 3.7.4 (#2509) ## 21.9b0 diff --git a/Pipfile b/Pipfile index 66ded8b8570..6c635a63c66 100644 --- a/Pipfile +++ b/Pipfile @@ -40,7 +40,7 @@ readme_renderer = "*" black = {editable = true, extras = ["d", "jupyter"], path = "."} [packages] -aiohttp = ">=3.6.0" +aiohttp = ">=3.7.4" platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" diff --git a/setup.py b/setup.py index 19df9beb24e..015f321eaa9 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def get_long_description() -> str: "mypy_extensions>=0.4.3", ], extras_require={ - "d": ["aiohttp>=3.6.0"], + "d": ["aiohttp>=3.7.4"], "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], "uvloop": ["uvloop>=0.15.2"], From 39b55f787cff41ef9726b256cfdbedefe6d5c716 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 25 Sep 2021 20:46:36 +0100 Subject: [PATCH 007/700] Add test to cover when unable to replace magics (#2471) Another follow-up from #2357, adding a test for uncovered code. --- src/black/handle_ipynb_magics.py | 7 ++++--- tests/test_ipynb.py | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index b18f8629136..63c8aafe35b 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -53,6 +53,7 @@ "%%writefile", ) ) +TOKEN_HEX = secrets.token_hex @dataclasses.dataclass(frozen=True) @@ -188,10 +189,10 @@ def get_token(src: str, magic: str) -> str: """ assert magic nbytes = max(len(magic) // 2 - 1, 1) - token = secrets.token_hex(nbytes) + token = TOKEN_HEX(nbytes) counter = 0 - while token in src: # pragma: nocover - token = secrets.token_hex(nbytes) + while token in src: + token = TOKEN_HEX(nbytes) counter += 1 if counter > 100: raise AssertionError( diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 038155e9270..12f176c9341 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -453,3 +453,12 @@ def test_ipynb_and_pyi_flags() -> None: assert isinstance(result.exception, SystemExit) expected = "Cannot pass both `pyi` and `ipynb` flags!\n" assert result.output == expected + + +def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None: + src = "%%time\na = 'foo'" + monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo") + with pytest.raises( + AssertionError, match="Black was not able to replace IPython magic" + ): + format_cell(src, fast=True, mode=JUPYTER_MODE) From 31a9d8a184a4dd2268df53672ec5dbc017a5eec9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 28 Sep 2021 18:41:35 -0400 Subject: [PATCH 008/700] Fix python_version markers in Pipfile.lock (#2511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This took way too much effort but in the end I was able to achieve a (mostly) functional Pipfile.lock ranging from 3.6 to 3.9 🎉 --- Pipfile | 4 ++-- Pipfile.lock | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Pipfile b/Pipfile index 6c635a63c66..c6cd8d41ef5 100644 --- a/Pipfile +++ b/Pipfile @@ -48,6 +48,6 @@ pathspec = ">=0.8.1" regex = ">=2020.1.8" tomli = ">=0.2.6, <2.0.0" typed-ast = "==1.4.2" -typing_extensions = {"python_version <" = "3.10","version >=" = "3.10.0.0"} +typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} black = {editable = true,extras = ["d"],path = "."} -dataclasses = {"python_version <" = "3.7","version >" = "0.1.3"} +dataclasses = {markers = "python_version < '3.7'", version = ">0.1.3"} diff --git a/Pipfile.lock b/Pipfile.lock index 0cbe9011981..22b66ba8ca3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8b28e41c5a63f0c30361d2a0ed29dc1e3f0468223ef150ae68586839e2ccf1c9" + "sha256": "192f075f04e702887745a3f19056b0172d83e4bc494fff4e0bcd6cfcafedd512" }, "pipfile-spec": 6, "requires": {}, @@ -102,9 +102,8 @@ "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" ], "index": "pypi", - "python_version <": "3.7", - "version": "==0.8", - "version >": "0.1.3" + "markers": "python_version < '3.7'", + "version": "==0.8" }, "idna": { "hashes": [ @@ -321,9 +320,8 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.10", - "version": "==3.10.0.0", - "version >=": "3.10.0.0" + "markers": "python_version < '3.10'", + "version": "==3.10.0.0" }, "yarl": { "hashes": [ @@ -1558,9 +1556,8 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.10", - "version": "==3.10.0.0", - "version >=": "3.10.0.0" + "markers": "python_version < '3.10'", + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ From 09915f4bd2d13652c089b9a96408b39116d82eb0 Mon Sep 17 00:00:00 2001 From: shaoran Date: Wed, 29 Sep 2021 02:31:29 +0200 Subject: [PATCH 009/700] Allow to pass the FileMode options in the vim plugin (#1319) --- CHANGES.md | 4 +++ autoload/black.vim | 49 +++++++++++++++++++++++++++++++++--- docs/integrations/editors.md | 2 ++ plugin/black.vim | 14 ++++++++++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1ff2cea84ca..61d3178f922 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,10 @@ - Remove dependency on aiohttp-cors (#2500) - Bump required aiohttp version to 3.7.4 (#2509) +### Integrations + +- Allow to pass `target_version` in the vim plugin (#1319) + ## 21.9b0 ### Packaging diff --git a/autoload/black.vim b/autoload/black.vim index 29d8f2b88e4..9ff5c2341fe 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -98,13 +98,47 @@ if _initialize_black_env(): import black import time -def Black(): +def get_target_version(tv): + if isinstance(tv, black.TargetVersion): + return tv + ret = None + try: + ret = black.TargetVersion[tv.upper()] + except KeyError: + print(f"WARNING: Target version {tv!r} not recognized by Black, using default target") + return ret + +def Black(**kwargs): + """ + kwargs allows you to override ``target_versions`` argument of + ``black.FileMode``. + + ``target_version`` needs to be cleaned because ``black.FileMode`` + expects the ``target_versions`` argument to be a set of TargetVersion enums. + + Allow kwargs["target_version"] to be a string to allow + to type it more quickly. + + Using also target_version instead of target_versions to remain + consistent to Black's documentation of the structure of pyproject.toml. + """ start = time.time() configs = get_configs() + + black_kwargs = {} + if "target_version" in kwargs: + target_version = kwargs["target_version"] + + if not isinstance(target_version, (list, set)): + target_version = [target_version] + target_version = set(filter(lambda x: x, map(lambda tv: get_target_version(tv), target_version))) + black_kwargs["target_versions"] = target_version + mode = black.FileMode( line_length=configs["line_length"], string_normalization=not configs["skip_string_normalization"], is_pyi=vim.current.buffer.name.endswith('.pyi'), + **black_kwargs, ) quiet = configs["quiet"] @@ -160,8 +194,17 @@ def BlackVersion(): EndPython3 -function black#Black() - :py3 Black() +function black#Black(...) + let kwargs = {} + for arg in a:000 + let arg_list = split(arg, '=') + let kwargs[arg_list[0]] = arg_list[1] + endfor +python3 << EOF +import vim +kwargs = vim.eval("kwargs") +EOF + :py3 Black(**kwargs) endfunction function black#BlackUpgrade() diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 6098631e2a0..d3be7c0ea84 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -116,6 +116,8 @@ Wing supports black via the OS Commands tool, as explained in the Wing documenta Commands and shortcuts: - `:Black` to format the entire file (ranges not supported); + - you can optionally pass `target_version=` with the same values as in the + command line. - `:BlackUpgrade` to upgrade _Black_ inside the virtualenv; - `:BlackVersion` to get the current version of _Black_ inside the virtualenv. diff --git a/plugin/black.vim b/plugin/black.vim index 3bdca62e6ad..90d2047790b 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -53,8 +53,20 @@ endif if !exists("g:black_quiet") let g:black_quiet = 0 endif +if !exists("g:black_target_version") + let g:black_target_version = "" +endif +function BlackComplete(ArgLead, CmdLine, CursorPos) + return [ +\ 'target_version=py27', +\ 'target_version=py36', +\ 'target_version=py37', +\ 'target_version=py38', +\ 'target_version=py39', +\ ] +endfunction -command! Black :call black#Black() +command! -nargs=* -complete=customlist,BlackComplete Black :call black#Black() command! BlackUpgrade :call black#BlackUpgrade() command! BlackVersion :call black#BlackVersion() From 0fd353f1639c580c32599bf435902d08dbd9a560 Mon Sep 17 00:00:00 2001 From: Fergus Mitchell Date: Wed, 29 Sep 2021 17:50:44 +0100 Subject: [PATCH 010/700] Add --workers CLI parameter (fixes #2513) (#2514) Fixes #2513 --- CHANGES.md | 4 ++++ src/black/__init__.py | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 61d3178f922..04e9edf692c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased +### _Black_ + +- Add new `--workers` parameter (#2514) + ### _Blackd_ - Remove dependency on aiohttp-cors (#2500) diff --git a/src/black/__init__.py b/src/black/__init__.py index 7fed1355f47..83a39234d38 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -95,6 +95,8 @@ def from_configuration( # Legacy name, left for integrations. FileMode = Mode +DEFAULT_WORKERS = os.cpu_count() + def read_pyproject_toml( ctx: click.Context, param: click.Parameter, value: Optional[str] @@ -318,6 +320,14 @@ def validate_regex( "editors that rely on using stdin." ), ) +@click.option( + "-W", + "--workers", + type=click.IntRange(min=1), + default=DEFAULT_WORKERS, + show_default=True, + help="Number of parallel workers", +) @click.option( "-q", "--quiet", @@ -383,6 +393,7 @@ def main( extend_exclude: Optional[Pattern], force_exclude: Optional[Pattern], stdin_filename: Optional[str], + workers: int, src: Tuple[str, ...], config: Optional[str], ) -> None: @@ -468,6 +479,7 @@ def main( write_back=write_back, mode=mode, report=report, + workers=workers, ) if verbose or not quiet: @@ -644,12 +656,17 @@ def reformat_one( def reformat_many( - sources: Set[Path], fast: bool, write_back: WriteBack, mode: Mode, report: "Report" + sources: Set[Path], + fast: bool, + write_back: WriteBack, + mode: Mode, + report: "Report", + workers: Optional[int], ) -> None: """Reformat multiple files using a ProcessPoolExecutor.""" executor: Executor loop = asyncio.get_event_loop() - worker_count = os.cpu_count() + worker_count = workers if workers is not None else DEFAULT_WORKERS if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 worker_count = min(worker_count, 60) From 3500e1cda5bef73ddc7eaf79be6c67c918738936 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 2 Oct 2021 19:37:32 -0400 Subject: [PATCH 011/700] MNT: remove unnecessary test deps + some refactoring (GH-2510) The main goals of this commit include: * improving consistency on how strict the test suite is -- Jelle has seen cases where a test did not fail to an incomplete test setup even though it should've * simplifying tests for both ease of creation and reading via parametrization and helpers * reorganizing the test suite by grouping more tests * dropping test suite dependencies that aren't strictly necessary The test suite could definitely do with more refactoring, but this is a good first pass. Anyway it would've gotten too big to review effectively if I did continue on this PR. Commit history before squash merge: * Drop parameterized dep and refactor format tests Since the test suite is already using pytest-only features we can drop the parameterized test dependency in favour of pytest's own offering. I also added an utility function called assert_format that makes it even easier to verify Black formats some code correctly. We already have great tooling if the case is very simple in test_format.py but any sort of complication makes it hard to use. Also if you're writing a non-standard test case, you have to be careful to include all of the steps so issues don't go undetected. assert_format aims to 1) improve consistency, 2) avoid wasted CPU cycles, and 3) avoid logical errors that hide issues. Finally, quite a few tests were either moved and/or simplified with the new setup. * Move file collection tests * Add assert_collected_sources helper function Testing source collection involves a lot of repetitive boilerplate, something that black.files.get_sources's signature does not help with. So to cut down on boilerplate like `report=black.Report()` I added a convenience function to tests/test_black.py which wraps black.get_sources. Its signature is designed to be much more lax to make it much easier to use. Somehow this leads to cutting 100 lines! Also IMO the test cases are much easier to read since it's more declarative than really procedural now. * Run isort on some test files * Move cache tests * Use pytest-style asserts & add parametrization * Drop now unnecessary test dependencies *pytest-cases might be interesting for further refactoring but I haven't been able to wrap my head around it for the time being. We can always revisit anyway. --- Pipfile | 3 - Pipfile.lock | 45 +- test_requirements.txt | 3 - tests/test_black.py | 1456 +++++++++++++++++------------------------ tests/test_format.py | 159 ++++- tests/util.py | 87 ++- 6 files changed, 786 insertions(+), 967 deletions(-) diff --git a/Pipfile b/Pipfile index c6cd8d41ef5..534ca50fa5d 100644 --- a/Pipfile +++ b/Pipfile @@ -7,11 +7,8 @@ verify_ssl = true # Testing related requirements. coverage = ">= 5.3" pytest = " >= 6.1.1" -pytest-mock = ">= 3.3.1" -pytest-cases = ">= 2.3.0" pytest-xdist = ">= 2.2.1" pytest-cov = ">= 2.11.1" -parameterized = ">= 0.7.4" tox = "*" # Linting related requirements. diff --git a/Pipfile.lock b/Pipfile.lock index 22b66ba8ca3..280e6498af1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "192f075f04e702887745a3f19056b0172d83e4bc494fff4e0bcd6cfcafedd512" + "sha256": "6dbdff058fac8e6492f9d64194e98e48e062946ec4c06f9bb7df517d1dd89ce8" }, "pipfile-spec": 6, "requires": {}, @@ -661,16 +661,8 @@ "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" ], "index": "pypi", - "python_version <": "3.7", - "version": "==0.8", - "version >": "0.1.3" - }, - "decopatch": { - "hashes": [ - "sha256:29a74d5d753423b188d5b537532da4f4b88e33ddccb95a8a20a5eff5b13265d4", - "sha256:c66b0815f15db04de7bb52b0b276432b76b7346fe7046f28033f48a14340d144" - ], - "version": "==1.4.8" + "markers": "python_version < '3.7'", + "version": "==0.8" }, "decorator": { "hashes": [ @@ -827,13 +819,6 @@ "markers": "python_version >= '3.6'", "version": "==23.1.0" }, - "makefun": { - "hashes": [ - "sha256:033eed65e2c1804fca84161a38d1fc8bb8650d32a89ac1c5dc7e54b2b2c2e88c", - "sha256:a19bddf07efb6bf92e3ccde5d593e49bc59001fd6c17cf7301d7a73a2647ae83" - ], - "version": "==1.11.3" - }, "markdown-it-py": { "hashes": [ "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3", @@ -1028,14 +1013,6 @@ "markers": "python_version >= '3.6'", "version": "==21.0" }, - "parameterized": { - "hashes": [ - "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c", - "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9" - ], - "index": "pypi", - "version": "==0.8.1" - }, "parso": { "hashes": [ "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", @@ -1169,14 +1146,6 @@ "index": "pypi", "version": "==6.2.4" }, - "pytest-cases": { - "hashes": [ - "sha256:13136269240615bc79041f8af8fc96e0e3e085da72dd22b18625451fda2443b8", - "sha256:a4abe0ec2b8acf8f8b5ab73060de72eac745c6ed9cfa317d59ae71b4a0bbbdf5" - ], - "index": "pypi", - "version": "==3.6.3" - }, "pytest-cov": { "hashes": [ "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", @@ -1193,14 +1162,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.3.0" }, - "pytest-mock": { - "hashes": [ - "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3", - "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62" - ], - "index": "pypi", - "version": "==3.6.1" - }, "pytest-xdist": { "hashes": [ "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5", diff --git a/test_requirements.txt b/test_requirements.txt index 31ab2d05fea..5bc494d5999 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,9 +1,6 @@ coverage >= 5.3 pre-commit pytest >= 6.1.1 -pytest-mock >= 3.3.1 -pytest-cases >= 2.3.0 pytest-xdist >= 2.2.1 pytest-cov >= 2.11.1 -parameterized >= 0.7.4 tox diff --git a/tests/test_black.py b/tests/test_black.py index 398a528bee9..f25db1b73d1 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1,69 +1,70 @@ #!/usr/bin/env python3 -import multiprocessing + import asyncio +import inspect +import io import logging +import multiprocessing +import os +import sys +import types +import unittest from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from dataclasses import replace -import inspect -import io from io import BytesIO -import os from pathlib import Path from platform import system -import regex as re -import sys from tempfile import TemporaryDirectory -import types from typing import ( Any, Callable, Dict, - List, Iterator, + List, + Optional, + Sequence, TypeVar, + Union, ) -import pytest -import unittest -from unittest.mock import patch, MagicMock -from parameterized import parameterized +from unittest.mock import MagicMock, patch import click +import pytest +import regex as re from click import unstyle from click.testing import CliRunner +from pathspec import PathSpec import black +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.debug import DebugVisitor -from black.output import diff, color_diff +from black.output import color_diff, diff from black.report import Report -import black.files - -from pathspec import PathSpec # Import other test classes from tests.util import ( - THIS_DIR, - change_directory, - read_data, + DATA_DIR, + DEFAULT_MODE, DETERMINISTIC_HEADER, + PY36_VERSIONS, + THIS_DIR, BlackBaseTestCase, - DEFAULT_MODE, - fs, - ff, + assert_format, + change_directory, dump_to_stderr, + ff, + fs, + read_data, ) - THIS_FILE = Path(__file__) -PY36_VERSIONS = { - TargetVersion.PY36, - TargetVersion.PY37, - TargetVersion.PY38, - TargetVersion.PY39, -} 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) T = TypeVar("T") R = TypeVar("R") @@ -114,34 +115,26 @@ def __init__(self) -> None: super().__init__(mix_stderr=False) -class BlackTestCase(BlackBaseTestCase): - def invokeBlack( - self, args: List[str], exit_code: int = 0, ignore_config: bool = True - ) -> None: - runner = BlackRunner() - if ignore_config: - args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args] - result = runner.invoke(black.main, args) - assert result.stdout_bytes is not None - assert result.stderr_bytes is not None - self.assertEqual( - result.exit_code, - exit_code, - msg=( - f"Failed with args: {args}\n" - f"stdout: {result.stdout_bytes.decode()!r}\n" - f"stderr: {result.stderr_bytes.decode()!r}\n" - f"exception: {result.exception}" - ), - ) +def invokeBlack( + args: List[str], exit_code: int = 0, ignore_config: bool = True +) -> None: + runner = BlackRunner() + if ignore_config: + args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args] + result = runner.invoke(black.main, args) + assert result.stdout_bytes is not None + assert result.stderr_bytes is not None + msg = ( + f"Failed with args: {args}\n" + f"stdout: {result.stdout_bytes.decode()!r}\n" + f"stderr: {result.stderr_bytes.decode()!r}\n" + f"exception: {result.exception}" + ) + assert result.exit_code == exit_code, msg - @patch("black.dump_to_file", dump_to_stderr) - def test_empty(self) -> None: - source = expected = "" - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) + +class BlackTestCase(BlackBaseTestCase): + invokeBlack = staticmethod(invokeBlack) def test_empty_ff(self) -> None: expected = "" @@ -266,32 +259,6 @@ def test_trailing_comma_optional_parens_stability3_pass2(self) -> None: 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_pep_572(self) -> None: - source, expected = read_data("pep_572") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - if sys.version_info >= (3, 8): - black.assert_equivalent(source, actual) - - @patch("black.dump_to_file", dump_to_stderr) - def test_pep_572_remove_parens(self) -> None: - source, expected = read_data("pep_572_remove_parens") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - if sys.version_info >= (3, 8): - black.assert_equivalent(source, actual) - - @patch("black.dump_to_file", dump_to_stderr) - def test_pep_572_do_not_remove_parens(self) -> None: - source, expected = read_data("pep_572_do_not_remove_parens") - # the AST safety checks will fail, but that's expected, just make sure no - # parentheses are touched - actual = black.format_str(source, mode=DEFAULT_MODE) - self.assertFormatEqual(expected, actual) - def test_pep_572_version_detection(self) -> None: source, _ = read_data("pep_572") root = black.lib2to3_parse(source) @@ -300,14 +267,6 @@ def test_pep_572_version_detection(self) -> None: versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) - @parameterized.expand([(3, 9), (3, 10)]) - def test_pep_572_newer_syntax(self, major: int, minor: int) -> None: - source, expected = read_data(f"pep_572_py{major}{minor}") - actual = fs(source, mode=DEFAULT_MODE) - self.assertFormatEqual(expected, actual) - if sys.version_info >= (major, minor): - black.assert_equivalent(source, actual) - def test_expression_ff(self) -> None: source, expected = read_data("expression") tmp_file = Path(black.dump_to_file(source)) @@ -369,15 +328,6 @@ def test_expression_diff_with_color(self) -> None: self.assertIn("\033[31m", actual) self.assertIn("\033[0m", actual) - @patch("black.dump_to_file", dump_to_stderr) - def test_pep_570(self) -> None: - source, expected = read_data("pep_570") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - if sys.version_info >= (3, 8): - black.assert_equivalent(source, actual) - def test_detect_pos_only_arguments(self) -> None: source, _ = read_data("pep_570") root = black.lib2to3_parse(source) @@ -390,52 +340,13 @@ def test_detect_pos_only_arguments(self) -> None: def test_string_quotes(self) -> None: source, expected = read_data("string_quotes") mode = black.Mode(experimental_string_processing=True) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) + assert_format(source, expected, mode) mode = replace(mode, string_normalization=False) not_normalized = fs(source, mode=mode) self.assertFormatEqual(source.replace("\\\n", ""), not_normalized) black.assert_equivalent(source, not_normalized) black.assert_stable(source, not_normalized, mode=mode) - @patch("black.dump_to_file", dump_to_stderr) - def test_docstring_no_string_normalization(self) -> None: - """Like test_docstring but with string normalization off.""" - source, expected = read_data("docstring_no_string_normalization") - mode = replace(DEFAULT_MODE, string_normalization=False) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) - - def test_long_strings_flag_disabled(self) -> None: - """Tests for turning off the string processing logic.""" - source, expected = read_data("long_strings_flag_disabled") - mode = replace(DEFAULT_MODE, experimental_string_processing=False) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_stable(expected, actual, mode) - - @patch("black.dump_to_file", dump_to_stderr) - def test_numeric_literals(self) -> None: - source, expected = read_data("numeric_literals") - mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) - - @patch("black.dump_to_file", dump_to_stderr) - def test_numeric_literals_ignoring_underscores(self) -> None: - source, expected = read_data("numeric_literals_skip_underscores") - mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) - def test_skip_magic_trailing_comma(self) -> None: source, _ = read_data("expression.py") expected, _ = read_data("expression_skip_magic_trailing_comma.diff") @@ -461,24 +372,6 @@ def test_skip_magic_trailing_comma(self) -> None: ) self.assertEqual(expected, actual, msg) - @pytest.mark.python2 - @patch("black.dump_to_file", dump_to_stderr) - def test_python2_print_function(self) -> None: - source, expected = read_data("python2_print_function") - mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27}) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) - - @patch("black.dump_to_file", dump_to_stderr) - def test_stub(self) -> None: - mode = replace(DEFAULT_MODE, is_pyi=True) - source, expected = read_data("stub.pyi") - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, mode) - @patch("black.dump_to_file", dump_to_stderr) def test_async_as_identifier(self) -> None: source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve() @@ -509,26 +402,6 @@ def test_python37(self) -> None: # but not on 3.6, because we use async as a reserved keyword self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123) - @patch("black.dump_to_file", dump_to_stderr) - def test_python38(self) -> None: - source, expected = read_data("python38") - actual = fs(source) - self.assertFormatEqual(expected, actual) - major, minor = sys.version_info[:2] - if major > 3 or (major == 3 and minor >= 8): - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_python39(self) -> None: - source, expected = read_data("python39") - actual = fs(source) - self.assertFormatEqual(expected, actual) - major, minor = sys.version_info[:2] - if major > 3 or (major == 3 and minor >= 9): - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - def test_tab_comment_indentation(self) -> None: contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n" contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n" @@ -1033,256 +906,67 @@ def err(msg: str, **kwargs: Any) -> None: self.assertTrue("Actual tree:" in out_str) self.assertEqual("".join(err_lines), "") - def test_cache_broken_file(self) -> None: - mode = DEFAULT_MODE - with cache_dir() as workspace: - cache_file = get_cache_file(mode) - with cache_file.open("w") as fobj: - fobj.write("this is not a pickle") - self.assertEqual(black.read_cache(mode), {}) - src = (workspace / "test.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - self.invokeBlack([str(src)]) - cache = black.read_cache(mode) - self.assertIn(str(src), cache) - - def test_cache_single_file_already_cached(self) -> None: - mode = DEFAULT_MODE + @event_loop() + @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError)) + def test_works_in_mono_process_only_environment(self) -> None: with cache_dir() as workspace: - src = (workspace / "test.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - black.write_cache({}, [src], mode) - self.invokeBlack([str(src)]) - with src.open("r") as fobj: - self.assertEqual(fobj.read(), "print('hello')") + for f in [ + (workspace / "one.py").resolve(), + (workspace / "two.py").resolve(), + ]: + f.write_text('print("hello")\n') + self.invokeBlack([str(workspace)]) @event_loop() - def test_cache_multiple_files(self) -> None: - mode = DEFAULT_MODE - with cache_dir() as workspace, patch( - "black.ProcessPoolExecutor", new=ThreadPoolExecutor - ): - one = (workspace / "one.py").resolve() - with one.open("w") as fobj: - fobj.write("print('hello')") - two = (workspace / "two.py").resolve() - with two.open("w") as fobj: - fobj.write("print('hello')") - black.write_cache({}, [one], mode) - self.invokeBlack([str(workspace)]) - with one.open("r") as fobj: - self.assertEqual(fobj.read(), "print('hello')") - with two.open("r") as fobj: - self.assertEqual(fobj.read(), 'print("hello")\n') - cache = black.read_cache(mode) - self.assertIn(str(one), cache) - self.assertIn(str(two), cache) + def test_check_diff_use_together(self) -> None: + with cache_dir(): + # Files which will be reformatted. + src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() + self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) + # Files which will not be reformatted. + src2 = (THIS_DIR / "data" / "composition.py").resolve() + self.invokeBlack([str(src2), "--diff", "--check"]) + # Multi file command. + self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) - def test_no_cache_when_writeback_diff(self) -> None: - mode = DEFAULT_MODE - with cache_dir() as workspace: - src = (workspace / "test.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - with patch("black.read_cache") as read_cache, patch( - "black.write_cache" - ) as write_cache: - self.invokeBlack([str(src), "--diff"]) - cache_file = get_cache_file(mode) - self.assertFalse(cache_file.exists()) - write_cache.assert_not_called() - read_cache.assert_not_called() + def test_no_files(self) -> None: + with cache_dir(): + # Without an argument, black exits with error code 0. + self.invokeBlack([]) - def test_no_cache_when_writeback_color_diff(self) -> None: - mode = DEFAULT_MODE + def test_broken_symlink(self) -> None: with cache_dir() as workspace: - src = (workspace / "test.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - with patch("black.read_cache") as read_cache, patch( - "black.write_cache" - ) as write_cache: - self.invokeBlack([str(src), "--diff", "--color"]) - cache_file = get_cache_file(mode) - self.assertFalse(cache_file.exists()) - write_cache.assert_not_called() - read_cache.assert_not_called() + symlink = workspace / "broken_link.py" + try: + symlink.symlink_to("nonexistent.py") + except OSError as e: + self.skipTest(f"Can't create symlinks: {e}") + self.invokeBlack([str(workspace.resolve())]) - @event_loop() - def test_output_locking_when_writeback_diff(self) -> None: + def test_single_file_force_pyi(self) -> None: + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) + contents, expected = read_data("force_pyi") with cache_dir() as workspace: - for tag in range(0, 4): - src = (workspace / f"test{tag}.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - with patch("black.Manager", wraps=multiprocessing.Manager) as mgr: - self.invokeBlack(["--diff", str(workspace)], exit_code=0) - # this isn't quite doing what we want, but if it _isn't_ - # called then we cannot be using the lock it provides - mgr.assert_called() + path = (workspace / "file.py").resolve() + with open(path, "w") as fh: + fh.write(contents) + self.invokeBlack([str(path), "--pyi"]) + with open(path, "r") as fh: + actual = fh.read() + # verify cache with --pyi is separate + pyi_cache = black.read_cache(pyi_mode) + self.assertIn(str(path), pyi_cache) + normal_cache = black.read_cache(DEFAULT_MODE) + self.assertNotIn(str(path), normal_cache) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(contents, actual) + black.assert_stable(contents, actual, pyi_mode) @event_loop() - def test_output_locking_when_writeback_color_diff(self) -> None: - with cache_dir() as workspace: - for tag in range(0, 4): - src = (workspace / f"test{tag}.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") - with patch("black.Manager", wraps=multiprocessing.Manager) as mgr: - self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0) - # this isn't quite doing what we want, but if it _isn't_ - # called then we cannot be using the lock it provides - mgr.assert_called() - - def test_no_cache_when_stdin(self) -> None: - mode = DEFAULT_MODE - with cache_dir(): - result = CliRunner().invoke( - black.main, ["-"], input=BytesIO(b"print('hello')") - ) - self.assertEqual(result.exit_code, 0) - cache_file = get_cache_file(mode) - self.assertFalse(cache_file.exists()) - - def test_read_cache_no_cachefile(self) -> None: - mode = DEFAULT_MODE - with cache_dir(): - self.assertEqual(black.read_cache(mode), {}) - - def test_write_cache_read_cache(self) -> None: - mode = DEFAULT_MODE - with cache_dir() as workspace: - src = (workspace / "test.py").resolve() - src.touch() - black.write_cache({}, [src], mode) - cache = black.read_cache(mode) - self.assertIn(str(src), cache) - self.assertEqual(cache[str(src)], black.get_cache_info(src)) - - def test_filter_cached(self) -> None: - with TemporaryDirectory() as workspace: - path = Path(workspace) - uncached = (path / "uncached").resolve() - cached = (path / "cached").resolve() - cached_but_changed = (path / "changed").resolve() - uncached.touch() - cached.touch() - cached_but_changed.touch() - cache = { - str(cached): black.get_cache_info(cached), - str(cached_but_changed): (0.0, 0), - } - todo, done = black.filter_cached( - cache, {uncached, cached, cached_but_changed} - ) - self.assertEqual(todo, {uncached, cached_but_changed}) - self.assertEqual(done, {cached}) - - def test_write_cache_creates_directory_if_needed(self) -> None: - mode = DEFAULT_MODE - with cache_dir(exists=False) as workspace: - self.assertFalse(workspace.exists()) - black.write_cache({}, [], mode) - self.assertTrue(workspace.exists()) - - @event_loop() - def test_failed_formatting_does_not_get_cached(self) -> None: - mode = DEFAULT_MODE - with cache_dir() as workspace, patch( - "black.ProcessPoolExecutor", new=ThreadPoolExecutor - ): - failing = (workspace / "failing.py").resolve() - with failing.open("w") as fobj: - fobj.write("not actually python") - clean = (workspace / "clean.py").resolve() - with clean.open("w") as fobj: - fobj.write('print("hello")\n') - self.invokeBlack([str(workspace)], exit_code=123) - cache = black.read_cache(mode) - self.assertNotIn(str(failing), cache) - self.assertIn(str(clean), cache) - - def test_write_cache_write_fail(self) -> None: - mode = DEFAULT_MODE - with cache_dir(), patch.object(Path, "open") as mock: - mock.side_effect = OSError - black.write_cache({}, [], mode) - - @event_loop() - @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError)) - def test_works_in_mono_process_only_environment(self) -> None: - with cache_dir() as workspace: - for f in [ - (workspace / "one.py").resolve(), - (workspace / "two.py").resolve(), - ]: - f.write_text('print("hello")\n') - self.invokeBlack([str(workspace)]) - - @event_loop() - def test_check_diff_use_together(self) -> None: - with cache_dir(): - # Files which will be reformatted. - src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() - self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) - # Files which will not be reformatted. - src2 = (THIS_DIR / "data" / "composition.py").resolve() - self.invokeBlack([str(src2), "--diff", "--check"]) - # Multi file command. - self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) - - def test_no_files(self) -> None: - with cache_dir(): - # Without an argument, black exits with error code 0. - self.invokeBlack([]) - - def test_broken_symlink(self) -> None: - with cache_dir() as workspace: - symlink = workspace / "broken_link.py" - try: - symlink.symlink_to("nonexistent.py") - except OSError as e: - self.skipTest(f"Can't create symlinks: {e}") - self.invokeBlack([str(workspace.resolve())]) - - def test_read_cache_line_lengths(self) -> None: - mode = DEFAULT_MODE - short_mode = replace(DEFAULT_MODE, line_length=1) - with cache_dir() as workspace: - path = (workspace / "file.py").resolve() - path.touch() - black.write_cache({}, [path], mode) - one = black.read_cache(mode) - self.assertIn(str(path), one) - two = black.read_cache(short_mode) - self.assertNotIn(str(path), two) - - def test_single_file_force_pyi(self) -> None: - pyi_mode = replace(DEFAULT_MODE, is_pyi=True) - contents, expected = read_data("force_pyi") - with cache_dir() as workspace: - path = (workspace / "file.py").resolve() - with open(path, "w") as fh: - fh.write(contents) - self.invokeBlack([str(path), "--pyi"]) - with open(path, "r") as fh: - actual = fh.read() - # verify cache with --pyi is separate - pyi_cache = black.read_cache(pyi_mode) - self.assertIn(str(path), pyi_cache) - normal_cache = black.read_cache(DEFAULT_MODE) - self.assertNotIn(str(path), normal_cache) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(contents, actual) - black.assert_stable(contents, actual, pyi_mode) - - @event_loop() - def test_multi_file_force_pyi(self) -> None: - reg_mode = DEFAULT_MODE - pyi_mode = replace(DEFAULT_MODE, is_pyi=True) - contents, expected = read_data("force_pyi") + def test_multi_file_force_pyi(self) -> None: + reg_mode = DEFAULT_MODE + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) + contents, expected = read_data("force_pyi") with cache_dir() as workspace: paths = [ (workspace / "file1.py").resolve(), @@ -1366,216 +1050,6 @@ def test_pipe_force_py36(self) -> None: actual = result.output self.assertFormatEqual(actual, expected) - def test_include_exclude(self) -> None: - path = THIS_DIR / "data" / "include_exclude_tests" - include = re.compile(r"\.pyi?$") - exclude = re.compile(r"/exclude/|/\.definitely_exclude/") - report = black.Report() - gitignore = PathSpec.from_lines("gitwildmatch", []) - sources: List[Path] = [] - expected = [ - Path(path / "b/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), - ] - this_abs = THIS_DIR.resolve() - sources.extend( - black.gen_python_files( - path.iterdir(), - this_abs, - include, - exclude, - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - def test_gitignore_used_as_default(self) -> None: - path = Path(THIS_DIR / "data" / "include_exclude_tests") - include = re.compile(r"\.pyi?$") - extend_exclude = re.compile(r"/exclude/") - src = str(path / "b/") - report = black.Report() - expected: List[Path] = [ - path / "b/.definitely_exclude/a.py", - path / "b/.definitely_exclude/a.pyi", - ] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=include, - exclude=None, - extend_exclude=extend_exclude, - force_exclude=None, - report=report, - stdin_filename=None, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_exclude_for_issue_1572(self) -> None: - # Exclude shouldn't touch files that were explicitly given to Black through the - # CLI. Exclude is supposed to only apply to the recursive discovery of files. - # https://github.com/psf/black/issues/1572 - path = THIS_DIR / "data" / "include_exclude_tests" - include = "" - exclude = r"/exclude/|a\.py" - src = str(path / "b/exclude/a.py") - report = black.Report() - expected = [Path(path / "b/exclude/a.py")] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(exclude), - extend_exclude=None, - force_exclude=None, - report=report, - stdin_filename=None, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_get_sources_with_stdin(self) -> None: - include = "" - exclude = r"/exclude/|a\.py" - src = "-" - report = black.Report() - expected = [Path("-")] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(exclude), - extend_exclude=None, - force_exclude=None, - report=report, - stdin_filename=None, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_get_sources_with_stdin_filename(self) -> None: - include = "" - exclude = r"/exclude/|a\.py" - src = "-" - report = black.Report() - stdin_filename = str(THIS_DIR / "data/collections.py") - expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(exclude), - extend_exclude=None, - force_exclude=None, - report=report, - stdin_filename=stdin_filename, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_get_sources_with_stdin_filename_and_exclude(self) -> None: - # Exclude shouldn't exclude stdin_filename since it is mimicking the - # file being passed directly. This is the same as - # test_exclude_for_issue_1572 - path = THIS_DIR / "data" / "include_exclude_tests" - include = "" - exclude = r"/exclude/|a\.py" - src = "-" - report = black.Report() - stdin_filename = str(path / "b/exclude/a.py") - expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(exclude), - extend_exclude=None, - force_exclude=None, - report=report, - stdin_filename=stdin_filename, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: - # Extend exclude shouldn't exclude stdin_filename since it is mimicking the - # file being passed directly. This is the same as - # test_exclude_for_issue_1572 - path = THIS_DIR / "data" / "include_exclude_tests" - include = "" - extend_exclude = r"/exclude/|a\.py" - src = "-" - report = black.Report() - stdin_filename = str(path / "b/exclude/a.py") - expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")] - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(""), - extend_exclude=re.compile(extend_exclude), - force_exclude=None, - report=report, - stdin_filename=stdin_filename, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) - def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: - # Force exclude should exclude the file when passing it through - # stdin_filename - path = THIS_DIR / "data" / "include_exclude_tests" - include = "" - force_exclude = r"/exclude/|a\.py" - src = "-" - report = black.Report() - stdin_filename = str(path / "b/exclude/a.py") - sources = list( - black.get_sources( - ctx=FakeContext(), - src=(src,), - quiet=True, - verbose=False, - include=re.compile(include), - exclude=re.compile(""), - extend_exclude=None, - force_exclude=re.compile(force_exclude), - report=report, - stdin_filename=stdin_filename, - ) - ) - self.assertEqual([], sorted(sources)) - def test_reformat_one_with_stdin(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1701,158 +1175,13 @@ def test_reformat_one_with_stdin_empty(self) -> None: pass # StringIO does not support detach assert output.getvalue() == "" - def test_gitignore_exclude(self) -> None: - path = THIS_DIR / "data" / "include_exclude_tests" - include = re.compile(r"\.pyi?$") - exclude = re.compile(r"") - report = black.Report() - gitignore = PathSpec.from_lines( - "gitwildmatch", ["exclude/", ".definitely_exclude"] - ) - sources: List[Path] = [] - expected = [ - Path(path / "b/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), - ] - this_abs = THIS_DIR.resolve() - sources.extend( - black.gen_python_files( - path.iterdir(), - this_abs, - include, - exclude, - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - def test_nested_gitignore(self) -> None: - path = Path(THIS_DIR / "data" / "nested_gitignore_tests") - include = re.compile(r"\.pyi?$") - exclude = re.compile(r"") - root_gitignore = black.files.get_gitignore(path) - report = black.Report() - expected: List[Path] = [ - Path(path / "x.py"), - Path(path / "root/b.py"), - Path(path / "root/c.py"), - Path(path / "root/child/c.py"), - ] - this_abs = THIS_DIR.resolve() - sources = list( - black.gen_python_files( - path.iterdir(), - this_abs, - include, - exclude, - None, - None, - report, - root_gitignore, - verbose=False, - quiet=False, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - def test_invalid_gitignore(self) -> None: - path = THIS_DIR / "data" / "invalid_gitignore_tests" - empty_config = path / "pyproject.toml" - result = BlackRunner().invoke( - black.main, ["--verbose", "--config", str(empty_config), str(path)] - ) - assert result.exit_code == 1 - assert result.stderr_bytes is not None - - gitignore = path / ".gitignore" - assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() - - def test_invalid_nested_gitignore(self) -> None: - path = THIS_DIR / "data" / "invalid_nested_gitignore_tests" - empty_config = path / "pyproject.toml" - result = BlackRunner().invoke( - black.main, ["--verbose", "--config", str(empty_config), str(path)] - ) - assert result.exit_code == 1 - assert result.stderr_bytes is not None - - gitignore = path / "a" / ".gitignore" - assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() - - def test_empty_include(self) -> None: - path = THIS_DIR / "data" / "include_exclude_tests" - report = black.Report() - gitignore = PathSpec.from_lines("gitwildmatch", []) - empty = re.compile(r"") - sources: List[Path] = [] - expected = [ - Path(path / "b/exclude/a.pie"), - Path(path / "b/exclude/a.py"), - Path(path / "b/exclude/a.pyi"), - Path(path / "b/dont_exclude/a.pie"), - Path(path / "b/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), - Path(path / "b/.definitely_exclude/a.pie"), - Path(path / "b/.definitely_exclude/a.py"), - Path(path / "b/.definitely_exclude/a.pyi"), - Path(path / ".gitignore"), - Path(path / "pyproject.toml"), - ] - this_abs = THIS_DIR.resolve() - sources.extend( - black.gen_python_files( - path.iterdir(), - this_abs, - empty, - re.compile(black.DEFAULT_EXCLUDES), - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - def test_extend_exclude(self) -> None: - path = THIS_DIR / "data" / "include_exclude_tests" - report = black.Report() - gitignore = PathSpec.from_lines("gitwildmatch", []) - sources: List[Path] = [] - expected = [ - Path(path / "b/exclude/a.py"), - Path(path / "b/dont_exclude/a.py"), - ] - this_abs = THIS_DIR.resolve() - sources.extend( - black.gen_python_files( - path.iterdir(), - this_abs, - re.compile(black.DEFAULT_INCLUDES), - re.compile(r"\.pyi$"), - re.compile(r"\.definitely_exclude"), - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - self.assertEqual(sorted(expected), sorted(sources)) - - def test_invalid_cli_regex(self) -> None: - for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: - self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) - - def test_required_version_matches_version(self) -> None: - self.invokeBlack( - ["--required-version", black.__version__], exit_code=0, ignore_config=True + def test_invalid_cli_regex(self) -> None: + for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: + self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) + + def test_required_version_matches_version(self) -> None: + self.invokeBlack( + ["--required-version", black.__version__], exit_code=0, ignore_config=True ) def test_required_version_does_not_match_version(self) -> None: @@ -1889,65 +1218,6 @@ def test_assert_equivalent_different_asts(self) -> None: with self.assertRaises(AssertionError): black.assert_equivalent("{}", "None") - def test_symlink_out_of_root_directory(self) -> None: - path = MagicMock() - root = THIS_DIR.resolve() - child = MagicMock() - include = re.compile(black.DEFAULT_INCLUDES) - exclude = re.compile(black.DEFAULT_EXCLUDES) - report = black.Report() - gitignore = PathSpec.from_lines("gitwildmatch", []) - # `child` should behave like a symlink which resolved path is clearly - # outside of the `root` directory. - path.iterdir.return_value = [child] - child.resolve.return_value = Path("/a/b/c") - child.as_posix.return_value = "/a/b/c" - child.is_symlink.return_value = True - try: - list( - black.gen_python_files( - path.iterdir(), - root, - include, - exclude, - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - except ValueError as ve: - self.fail(f"`get_python_files_in_dir()` failed: {ve}") - path.iterdir.assert_called_once() - child.resolve.assert_called_once() - child.is_symlink.assert_called_once() - # `child` should behave like a strange file which resolved path is clearly - # outside of the `root` directory. - child.is_symlink.return_value = False - with self.assertRaises(ValueError): - list( - black.gen_python_files( - path.iterdir(), - root, - include, - exclude, - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - path.iterdir.assert_called() - self.assertEqual(path.iterdir.call_count, 2) - child.resolve.assert_called() - self.assertEqual(child.resolve.call_count, 2) - child.is_symlink.assert_called() - self.assertEqual(child.is_symlink.call_count, 2) - def test_shhh_click(self) -> None: try: from click import _unicodefun @@ -2270,31 +1540,497 @@ def test_code_option_parent_config(self) -> None: ), "Incorrect config loaded." -with open(black.__file__, "r", encoding="utf-8") as _bf: - black_source_lines = _bf.readlines() +class TestCaching: + def test_cache_broken_file(self) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace: + cache_file = get_cache_file(mode) + cache_file.write_text("this is not a pickle") + assert black.read_cache(mode) == {} + src = (workspace / "test.py").resolve() + src.write_text("print('hello')") + invokeBlack([str(src)]) + cache = black.read_cache(mode) + assert str(src) in cache + def test_cache_single_file_already_cached(self) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace: + src = (workspace / "test.py").resolve() + src.write_text("print('hello')") + black.write_cache({}, [src], mode) + invokeBlack([str(src)]) + assert src.read_text() == "print('hello')" -def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable: - """Show function calls `from black/__init__.py` as they happen. + @event_loop() + def test_cache_multiple_files(self) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace, patch( + "black.ProcessPoolExecutor", new=ThreadPoolExecutor + ): + one = (workspace / "one.py").resolve() + with one.open("w") as fobj: + fobj.write("print('hello')") + two = (workspace / "two.py").resolve() + with two.open("w") as fobj: + fobj.write("print('hello')") + black.write_cache({}, [one], mode) + invokeBlack([str(workspace)]) + with one.open("r") as fobj: + assert fobj.read() == "print('hello')" + with two.open("r") as fobj: + assert fobj.read() == 'print("hello")\n' + cache = black.read_cache(mode) + assert str(one) in cache + assert str(two) in cache - Register this with `sys.settrace()` in a test you're debugging. - """ - if event != "call": - return tracefunc + @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"]) + def test_no_cache_when_writeback_diff(self, color: bool) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace: + src = (workspace / "test.py").resolve() + with src.open("w") as fobj: + fobj.write("print('hello')") + with patch("black.read_cache") as read_cache, patch( + "black.write_cache" + ) as write_cache: + cmd = [str(src), "--diff"] + if color: + cmd.append("--color") + invokeBlack(cmd) + cache_file = get_cache_file(mode) + assert cache_file.exists() is False + write_cache.assert_not_called() + read_cache.assert_not_called() - stack = len(inspect.stack()) - 19 - stack *= 2 - filename = frame.f_code.co_filename - lineno = frame.f_lineno - func_sig_lineno = lineno - 1 - funcname = black_source_lines[func_sig_lineno].strip() - while funcname.startswith("@"): - func_sig_lineno += 1 - funcname = black_source_lines[func_sig_lineno].strip() - if "black/__init__.py" in filename: - print(f"{' ' * stack}{lineno}:{funcname}") - return tracefunc + @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"]) + @event_loop() + def test_output_locking_when_writeback_diff(self, color: bool) -> None: + with cache_dir() as workspace: + for tag in range(0, 4): + src = (workspace / f"test{tag}.py").resolve() + with src.open("w") as fobj: + fobj.write("print('hello')") + with patch("black.Manager", wraps=multiprocessing.Manager) as mgr: + cmd = ["--diff", str(workspace)] + if color: + cmd.append("--color") + invokeBlack(cmd, exit_code=0) + # this isn't quite doing what we want, but if it _isn't_ + # called then we cannot be using the lock it provides + mgr.assert_called() + def test_no_cache_when_stdin(self) -> None: + mode = DEFAULT_MODE + with cache_dir(): + result = CliRunner().invoke( + black.main, ["-"], input=BytesIO(b"print('hello')") + ) + assert not result.exit_code + cache_file = get_cache_file(mode) + assert not cache_file.exists() -if __name__ == "__main__": - unittest.main(module="test_black") + def test_read_cache_no_cachefile(self) -> None: + mode = DEFAULT_MODE + with cache_dir(): + assert black.read_cache(mode) == {} + + def test_write_cache_read_cache(self) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace: + src = (workspace / "test.py").resolve() + src.touch() + black.write_cache({}, [src], mode) + cache = black.read_cache(mode) + assert str(src) in cache + assert cache[str(src)] == black.get_cache_info(src) + + def test_filter_cached(self) -> None: + with TemporaryDirectory() as workspace: + path = Path(workspace) + uncached = (path / "uncached").resolve() + cached = (path / "cached").resolve() + cached_but_changed = (path / "changed").resolve() + uncached.touch() + cached.touch() + cached_but_changed.touch() + cache = { + str(cached): black.get_cache_info(cached), + str(cached_but_changed): (0.0, 0), + } + todo, done = black.filter_cached( + cache, {uncached, cached, cached_but_changed} + ) + assert todo == {uncached, cached_but_changed} + assert done == {cached} + + def test_write_cache_creates_directory_if_needed(self) -> None: + mode = DEFAULT_MODE + with cache_dir(exists=False) as workspace: + assert not workspace.exists() + black.write_cache({}, [], mode) + assert workspace.exists() + + @event_loop() + def test_failed_formatting_does_not_get_cached(self) -> None: + mode = DEFAULT_MODE + with cache_dir() as workspace, patch( + "black.ProcessPoolExecutor", new=ThreadPoolExecutor + ): + failing = (workspace / "failing.py").resolve() + with failing.open("w") as fobj: + fobj.write("not actually python") + clean = (workspace / "clean.py").resolve() + with clean.open("w") as fobj: + fobj.write('print("hello")\n') + invokeBlack([str(workspace)], exit_code=123) + cache = black.read_cache(mode) + assert str(failing) not in cache + assert str(clean) in cache + + def test_write_cache_write_fail(self) -> None: + mode = DEFAULT_MODE + with cache_dir(), patch.object(Path, "open") as mock: + mock.side_effect = OSError + black.write_cache({}, [], mode) + + def test_read_cache_line_lengths(self) -> None: + mode = DEFAULT_MODE + short_mode = replace(DEFAULT_MODE, line_length=1) + with cache_dir() as workspace: + path = (workspace / "file.py").resolve() + path.touch() + black.write_cache({}, [path], mode) + one = black.read_cache(mode) + assert str(path) in one + two = black.read_cache(short_mode) + assert str(path) not in two + + +def assert_collected_sources( + src: Sequence[Union[str, Path]], + expected: Sequence[Union[str, Path]], + *, + exclude: Optional[str] = None, + include: Optional[str] = None, + extend_exclude: Optional[str] = None, + force_exclude: Optional[str] = None, + stdin_filename: Optional[str] = None, +) -> None: + gs_src = tuple(str(Path(s)) for s in src) + gs_expected = [Path(s) for s in expected] + gs_exclude = None if exclude is None else compile_pattern(exclude) + gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include) + gs_extend_exclude = ( + None if extend_exclude is None else compile_pattern(extend_exclude) + ) + gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude) + collected = black.get_sources( + ctx=FakeContext(), + src=gs_src, + quiet=False, + verbose=False, + include=gs_include, + exclude=gs_exclude, + extend_exclude=gs_extend_exclude, + force_exclude=gs_force_exclude, + report=black.Report(), + stdin_filename=stdin_filename, + ) + assert sorted(list(collected)) == sorted(gs_expected) + + +class TestFileCollection: + def test_include_exclude(self) -> None: + path = THIS_DIR / "data" / "include_exclude_tests" + src = [path] + expected = [ + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/dont_exclude/a.pyi"), + ] + assert_collected_sources( + src, + expected, + include=r"\.pyi?$", + exclude=r"/exclude/|/\.definitely_exclude/", + ) + + def test_gitignore_used_as_default(self) -> None: + base = Path(DATA_DIR / "include_exclude_tests") + expected = [ + base / "b/.definitely_exclude/a.py", + base / "b/.definitely_exclude/a.pyi", + ] + src = [base / "b/"] + assert_collected_sources(src, expected, extend_exclude=r"/exclude/") + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_exclude_for_issue_1572(self) -> None: + # Exclude shouldn't touch files that were explicitly given to Black through the + # CLI. Exclude is supposed to only apply to the recursive discovery of files. + # https://github.com/psf/black/issues/1572 + path = DATA_DIR / "include_exclude_tests" + src = [path / "b/exclude/a.py"] + expected = [path / "b/exclude/a.py"] + assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py") + + def test_gitignore_exclude(self) -> None: + path = THIS_DIR / "data" / "include_exclude_tests" + include = re.compile(r"\.pyi?$") + exclude = re.compile(r"") + report = black.Report() + gitignore = PathSpec.from_lines( + "gitwildmatch", ["exclude/", ".definitely_exclude"] + ) + sources: List[Path] = [] + expected = [ + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/dont_exclude/a.pyi"), + ] + this_abs = THIS_DIR.resolve() + sources.extend( + black.gen_python_files( + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, + ) + ) + assert sorted(expected) == sorted(sources) + + def test_nested_gitignore(self) -> None: + path = Path(THIS_DIR / "data" / "nested_gitignore_tests") + include = re.compile(r"\.pyi?$") + exclude = re.compile(r"") + root_gitignore = black.files.get_gitignore(path) + report = black.Report() + expected: List[Path] = [ + Path(path / "x.py"), + Path(path / "root/b.py"), + Path(path / "root/c.py"), + Path(path / "root/child/c.py"), + ] + this_abs = THIS_DIR.resolve() + sources = list( + black.gen_python_files( + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + root_gitignore, + verbose=False, + quiet=False, + ) + ) + assert sorted(expected) == sorted(sources) + + def test_invalid_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + + def test_invalid_nested_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_nested_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / "a" / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + + def test_empty_include(self) -> None: + path = DATA_DIR / "include_exclude_tests" + src = [path] + expected = [ + Path(path / "b/exclude/a.pie"), + Path(path / "b/exclude/a.py"), + Path(path / "b/exclude/a.pyi"), + Path(path / "b/dont_exclude/a.pie"), + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/dont_exclude/a.pyi"), + Path(path / "b/.definitely_exclude/a.pie"), + Path(path / "b/.definitely_exclude/a.py"), + Path(path / "b/.definitely_exclude/a.pyi"), + Path(path / ".gitignore"), + Path(path / "pyproject.toml"), + ] + # Setting exclude explicitly to an empty string to block .gitignore usage. + assert_collected_sources(src, expected, include="", exclude="") + + def test_extend_exclude(self) -> None: + path = DATA_DIR / "include_exclude_tests" + src = [path] + expected = [ + Path(path / "b/exclude/a.py"), + Path(path / "b/dont_exclude/a.py"), + ] + assert_collected_sources( + src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude" + ) + + def test_symlink_out_of_root_directory(self) -> None: + path = MagicMock() + root = THIS_DIR.resolve() + child = MagicMock() + include = re.compile(black.DEFAULT_INCLUDES) + exclude = re.compile(black.DEFAULT_EXCLUDES) + report = black.Report() + gitignore = PathSpec.from_lines("gitwildmatch", []) + # `child` should behave like a symlink which resolved path is clearly + # outside of the `root` directory. + path.iterdir.return_value = [child] + child.resolve.return_value = Path("/a/b/c") + child.as_posix.return_value = "/a/b/c" + child.is_symlink.return_value = True + try: + list( + black.gen_python_files( + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, + ) + ) + except ValueError as ve: + pytest.fail(f"`get_python_files_in_dir()` failed: {ve}") + path.iterdir.assert_called_once() + child.resolve.assert_called_once() + child.is_symlink.assert_called_once() + # `child` should behave like a strange file which resolved path is clearly + # outside of the `root` directory. + child.is_symlink.return_value = False + with pytest.raises(ValueError): + list( + black.gen_python_files( + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, + ) + ) + path.iterdir.assert_called() + assert path.iterdir.call_count == 2 + child.resolve.assert_called() + assert child.resolve.call_count == 2 + child.is_symlink.assert_called() + assert child.is_symlink.call_count == 2 + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin(self) -> None: + src = ["-"] + expected = ["-"] + assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py") + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename(self) -> None: + src = ["-"] + stdin_filename = str(THIS_DIR / "data/collections.py") + expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"] + assert_collected_sources( + src, + expected, + exclude=r"/exclude/a\.py", + stdin_filename=stdin_filename, + ) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename_and_exclude(self) -> None: + # Exclude shouldn't exclude stdin_filename since it is mimicking the + # file being passed directly. This is the same as + # test_exclude_for_issue_1572 + path = DATA_DIR / "include_exclude_tests" + src = ["-"] + stdin_filename = str(path / "b/exclude/a.py") + expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"] + assert_collected_sources( + src, + expected, + exclude=r"/exclude/|a\.py", + stdin_filename=stdin_filename, + ) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: + # Extend exclude shouldn't exclude stdin_filename since it is mimicking the + # file being passed directly. This is the same as + # test_exclude_for_issue_1572 + src = ["-"] + path = THIS_DIR / "data" / "include_exclude_tests" + stdin_filename = str(path / "b/exclude/a.py") + expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"] + assert_collected_sources( + src, + expected, + extend_exclude=r"/exclude/|a\.py", + stdin_filename=stdin_filename, + ) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: + # Force exclude should exclude the file when passing it through + # stdin_filename + path = THIS_DIR / "data" / "include_exclude_tests" + stdin_filename = str(path / "b/exclude/a.py") + assert_collected_sources( + src=["-"], + expected=[], + force_exclude=r"/exclude/|a\.py", + stdin_filename=stdin_filename, + ) + + +with open(black.__file__, "r", encoding="utf-8") as _bf: + black_source_lines = _bf.readlines() + + +def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable: + """Show function calls `from black/__init__.py` as they happen. + + Register this with `sys.settrace()` in a test you're debugging. + """ + if event != "call": + return tracefunc + + stack = len(inspect.stack()) - 19 + stack *= 2 + filename = frame.f_code.co_filename + lineno = frame.f_lineno + func_sig_lineno = lineno - 1 + funcname = black_source_lines[func_sig_lineno].strip() + while funcname.startswith("@"): + func_sig_lineno += 1 + funcname = black_source_lines[func_sig_lineno].strip() + if "black/__init__.py" in filename: + print(f"{' ' * stack}{lineno}:{funcname}") + return tracefunc diff --git a/tests/test_format.py b/tests/test_format.py index fc9678ad27c..a659382092a 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,16 +1,17 @@ +from dataclasses import replace +from typing import Any, Iterator from unittest.mock import patch -import black import pytest -from parameterized import parameterized +import black from tests.util import ( - BlackBaseTestCase, - fs, DEFAULT_MODE, + PY36_VERSIONS, + THIS_DIR, + assert_format, dump_to_stderr, read_data, - THIS_DIR, ) SIMPLE_CASES = [ @@ -113,33 +114,121 @@ ] -class TestSimpleFormat(BlackBaseTestCase): - @parameterized.expand(SIMPLE_CASES_PY2) - @pytest.mark.python2 - @patch("black.dump_to_file", dump_to_stderr) - def test_simple_format_py2(self, filename: str) -> None: - self.check_file(filename, DEFAULT_MODE) - - @parameterized.expand(SIMPLE_CASES) - @patch("black.dump_to_file", dump_to_stderr) - def test_simple_format(self, filename: str) -> None: - self.check_file(filename, DEFAULT_MODE) - - @parameterized.expand(EXPERIMENTAL_STRING_PROCESSING_CASES) - @patch("black.dump_to_file", dump_to_stderr) - def test_experimental_format(self, filename: str) -> None: - self.check_file(filename, black.Mode(experimental_string_processing=True)) - - @parameterized.expand(SOURCES) - @patch("black.dump_to_file", dump_to_stderr) - def test_source_is_formatted(self, filename: str) -> None: - path = THIS_DIR.parent / filename - self.check_file(str(path), DEFAULT_MODE, data=False) - - def check_file(self, filename: str, mode: black.Mode, *, data: bool = True) -> None: - source, expected = read_data(filename, data=data) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - if source != actual: - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) +@pytest.fixture(autouse=True) +def patch_dump_to_file(request: Any) -> Iterator[None]: + with patch("black.dump_to_file", dump_to_stderr): + yield + + +def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None: + source, expected = read_data(filename, data=data) + assert_format(source, expected, mode, fast=False) + + +@pytest.mark.parametrize("filename", SIMPLE_CASES_PY2) +@pytest.mark.python2 +def test_simple_format_py2(filename: str) -> None: + check_file(filename, DEFAULT_MODE) + + +@pytest.mark.parametrize("filename", SIMPLE_CASES) +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", SOURCES) +def test_source_is_formatted(filename: str) -> None: + path = THIS_DIR.parent / filename + check_file(str(path), DEFAULT_MODE, data=False) + + +# =============== # +# Complex cases +# ============= # + + +def test_empty() -> None: + source = expected = "" + assert_format(source, expected) + + +def test_pep_572() -> None: + source, expected = read_data("pep_572") + assert_format(source, expected, minimum_version=(3, 8)) + + +def test_pep_572_remove_parens() -> None: + source, expected = read_data("pep_572_remove_parens") + assert_format(source, expected, minimum_version=(3, 8)) + + +def test_pep_572_do_not_remove_parens() -> None: + source, expected = read_data("pep_572_do_not_remove_parens") + # the AST safety checks will fail, but that's expected, just make sure no + # parentheses are touched + assert_format(source, expected, fast=True) + + +@pytest.mark.parametrize("major, minor", [(3, 9), (3, 10)]) +def test_pep_572_newer_syntax(major: int, minor: int) -> None: + source, expected = read_data(f"pep_572_py{major}{minor}") + assert_format(source, expected, minimum_version=(major, minor)) + + +def test_pep_570() -> None: + source, expected = read_data("pep_570") + assert_format(source, expected, minimum_version=(3, 8)) + + +def test_docstring_no_string_normalization() -> None: + """Like test_docstring but with string normalization off.""" + source, expected = read_data("docstring_no_string_normalization") + mode = replace(DEFAULT_MODE, string_normalization=False) + assert_format(source, expected, mode) + + +def test_long_strings_flag_disabled() -> None: + """Tests for turning off the string processing logic.""" + source, expected = read_data("long_strings_flag_disabled") + mode = replace(DEFAULT_MODE, experimental_string_processing=False) + assert_format(source, expected, mode) + + +def test_numeric_literals() -> None: + source, expected = read_data("numeric_literals") + mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) + assert_format(source, expected, mode) + + +def test_numeric_literals_ignoring_underscores() -> None: + source, expected = read_data("numeric_literals_skip_underscores") + mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) + assert_format(source, expected, mode) + + +@pytest.mark.python2 +def test_python2_print_function() -> None: + source, expected = read_data("python2_print_function") + mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY27}) + assert_format(source, expected, mode) + + +def test_stub() -> None: + mode = replace(DEFAULT_MODE, is_pyi=True) + source, expected = read_data("stub.pyi") + assert_format(source, expected, mode) + + +def test_python38() -> None: + source, expected = read_data("python38") + assert_format(source, expected, minimum_version=(3, 8)) + + +def test_python39() -> None: + source, expected = read_data("python39") + assert_format(source, expected, minimum_version=(3, 9)) diff --git a/tests/util.py b/tests/util.py index e83017f5ad3..84e98bb0fbd 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,58 +1,97 @@ import os +import sys import unittest -from pathlib import Path -from typing import Iterator, List, Tuple, Any from contextlib import contextmanager from functools import partial +from pathlib import Path +from typing import Any, Iterator, List, Optional, Tuple import black -from black.output import out, err from black.debug import DebugVisitor +from black.mode import TargetVersion +from black.output import err, out THIS_DIR = Path(__file__).parent +DATA_DIR = THIS_DIR / "data" PROJECT_ROOT = THIS_DIR.parent EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" DETERMINISTIC_HEADER = "[Deterministic header]" +PY36_VERSIONS = { + TargetVersion.PY36, + TargetVersion.PY37, + TargetVersion.PY38, + TargetVersion.PY39, +} DEFAULT_MODE = black.Mode() ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) fs = partial(black.format_str, mode=DEFAULT_MODE) +def _assert_format_equal(expected: str, actual: str) -> None: + if actual != expected and not os.environ.get("SKIP_AST_PRINT"): + bdv: DebugVisitor[Any] + out("Expected tree:", fg="green") + try: + exp_node = black.lib2to3_parse(expected) + bdv = DebugVisitor() + list(bdv.visit(exp_node)) + except Exception as ve: + err(str(ve)) + out("Actual tree:", fg="red") + try: + exp_node = black.lib2to3_parse(actual) + bdv = DebugVisitor() + list(bdv.visit(exp_node)) + except Exception as ve: + err(str(ve)) + + assert actual == expected + + +def assert_format( + source: str, + expected: str, + mode: black.Mode = DEFAULT_MODE, + *, + fast: bool = False, + minimum_version: Optional[Tuple[int, int]] = None, +) -> None: + """Convenience function to check that Black formats as expected. + + You can pass @minimum_version if you're passing code with newer syntax to guard + safety guards so they don't just crash with a SyntaxError. Please note this is + separate from TargetVerson Mode configuration. + """ + actual = black.format_str(source, mode=mode) + _assert_format_equal(expected, actual) + # It's not useful to run safety checks if we're expecting no changes anyway. The + # assertion right above will raise if reality does actually make changes. This just + # avoids wasted CPU cycles. + if not fast and source != expected: + # Unfortunately the AST equivalence check relies on the built-in ast module + # being able to parse the code being formatted. This doesn't always work out + # when checking modern code on older versions. + if minimum_version is None or sys.version_info >= minimum_version: + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode=mode) + + def dump_to_stderr(*output: str) -> str: return "\n" + "\n".join(output) + "\n" class BlackBaseTestCase(unittest.TestCase): - maxDiff = None - _diffThreshold = 2 ** 20 - def assertFormatEqual(self, expected: str, actual: str) -> None: - if actual != expected and not os.environ.get("SKIP_AST_PRINT"): - bdv: DebugVisitor[Any] - out("Expected tree:", fg="green") - try: - exp_node = black.lib2to3_parse(expected) - bdv = DebugVisitor() - list(bdv.visit(exp_node)) - except Exception as ve: - err(str(ve)) - out("Actual tree:", fg="red") - try: - exp_node = black.lib2to3_parse(actual) - bdv = DebugVisitor() - list(bdv.visit(exp_node)) - except Exception as ve: - err(str(ve)) - self.assertMultiLineEqual(expected, actual) + _assert_format_equal(expected, actual) def read_data(name: str, data: bool = True) -> Tuple[str, str]: """read_data('test_name') -> 'input', 'output'""" if not name.endswith((".py", ".pyi", ".out", ".diff")): name += ".py" - base_dir = THIS_DIR / "data" if data else PROJECT_ROOT + base_dir = DATA_DIR if data else PROJECT_ROOT return read_data_from_file(base_dir / name) From 872bb9474efe683efaed1626c4c0172738634f28 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 4 Oct 2021 20:36:57 -0400 Subject: [PATCH 012/700] Bump typed-ast minimum to 1.4.3 for 3.10 compat (#2519) --- CHANGES.md | 1 + Pipfile | 2 +- Pipfile.lock | 6 +++--- setup.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 04e9edf692c..f5b11de4803 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Black_ - Add new `--workers` parameter (#2514) +- Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) ### _Blackd_ diff --git a/Pipfile b/Pipfile index 534ca50fa5d..90e8f62d666 100644 --- a/Pipfile +++ b/Pipfile @@ -44,7 +44,7 @@ mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" regex = ">=2020.1.8" tomli = ">=0.2.6, <2.0.0" -typed-ast = "==1.4.2" +typed-ast = "==1.4.3" typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} black = {editable = true,extras = ["d"],path = "."} dataclasses = {markers = "python_version < '3.7'", version = ">0.1.3"} diff --git a/Pipfile.lock b/Pipfile.lock index 280e6498af1..2ddeca88fff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6dbdff058fac8e6492f9d64194e98e48e062946ec4c06f9bb7df517d1dd89ce8" + "sha256": "4baa020356174f89177af103f1966928e7b9c2a69df3a9d4e8070eb83ee19387" }, "pipfile-spec": 6, "requires": {}, @@ -311,7 +311,7 @@ "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.4.3" }, "typing-extensions": { "hashes": [ @@ -1484,7 +1484,7 @@ "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.4.3" }, "types-dataclasses": { "hashes": [ diff --git a/setup.py b/setup.py index 015f321eaa9..de84dc37bb8 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ def get_long_description() -> str: extras_require={ "d": ["aiohttp>=3.7.4"], "colorama": ["colorama>=0.4.3"], - "python2": ["typed-ast>=1.4.2"], + "python2": ["typed-ast>=1.4.3"], "uvloop": ["uvloop>=0.15.2"], "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"], }, From 3b2a7d196bc1984aed194cca26a7900968ce4409 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Tue, 5 Oct 2021 23:22:56 +0200 Subject: [PATCH 013/700] chore(ci): use official Python 3.10 (#2521) Python 3.10 (final) was released yesterday and is now available on GHA! --- .github/workflows/fuzz.yml | 2 +- .github/workflows/primer.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 6b3ca6bb068..146277a7312 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index 01eb4ef6187..5fa6ac066e3 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, windows-latest] steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e6b4a7a72d..296ac34a3fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: From 2f3fa1f6d0cbc2a3f31c7440c422da173b068e7b Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 12 Oct 2021 15:45:58 +1100 Subject: [PATCH 014/700] Fix feature detection for positional-only arguments in lambdas (#2532) --- CHANGES.md | 1 + src/black/__init__.py | 6 +++++- tests/test_black.py | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f5b11de4803..864f0a54410 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Black_ - Add new `--workers` parameter (#2514) +- Fixed feature detection for positional-only arguments in lambdas (#2532) - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 83a39234d38..fdbaf040d64 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1123,7 +1123,11 @@ def get_features_used(node: Node) -> Set[Feature]: features.add(Feature.NUMERIC_UNDERSCORES) elif n.type == token.SLASH: - if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}: + if n.parent and n.parent.type in { + syms.typedargslist, + syms.arglist, + syms.varargslist, + }: features.add(Feature.POS_ONLY_ARGUMENTS) elif n.type == token.COLONEQUAL: diff --git a/tests/test_black.py b/tests/test_black.py index f25db1b73d1..beb56cf1fdb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -805,6 +805,10 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse(expected) self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("lambda a, /, b: ...") + self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) + node = black.lib2to3_parse("def fn(a, /, b): ...") + self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) def test_get_future_imports(self) -> None: node = black.lib2to3_parse("\n") From 847d468b828faff7daae297d8a20d899d2259824 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Tue, 19 Oct 2021 18:52:10 +0100 Subject: [PATCH 015/700] bump sphinx so it works on Python3.10 (#2546) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 4c5b700412a..296efc5cc84 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.15.1 -Sphinx==4.1.2 +Sphinx==4.2.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From c75abed63ef284d7de54db87777951d6c668eefc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 21 Oct 2021 08:02:38 -0700 Subject: [PATCH 016/700] Define a stability policy (#2529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2394. Eventually fixes #517. This is essentially @pradyunsg's suggestion from #2394. I suggest that at the same time we start the formal stability policy, we take a few other disruptive steps and drop Python 2 and the "b" marker. Co-authored-by: Pradyun Gedam Co-authored-by: Łukasz Langa --- CHANGES.md | 1 + docs/faq.md | 12 +++++++++--- docs/the_black_code_style/index.rst | 30 ++++++++++++++++++++++++++--- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 864f0a54410..2a3a60f82c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Document stability policy, that will apply for non-beta releases (#2529) - Add new `--workers` parameter (#2514) - Fixed feature detection for positional-only arguments in lambdas (#2532) - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) diff --git a/docs/faq.md b/docs/faq.md index c361addf7ae..9fe53922b6d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -31,6 +31,10 @@ pragmatism. However, _Black_ is still in beta so style changes are both planned still proposed on the issue tracker. 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 +styles, using the `--future` flag. + ## Why is my file not formatted? Most likely because it is ignored in `.gitignore` or excluded with configuration. See @@ -71,9 +75,11 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra -to format Python 2 files too! There are no current plans to drop support, but most -likely it is bound to happen. Sometime. Eventually. In terms of running _Black_ though, -Python 3.6 or newer is required. +to format Python 2 files too! In terms of running _Black_ though, Python 3.6 or newer is +required. + +Note that this support will be dropped in the first stable release, expected for +January 2022. See [The Black Code Style](the_black_code_style/index.rst) for details. ## Why does my linter or typechecker complain after I format my code? diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst index 4693437be8b..7c2e1753937 100644 --- a/docs/the_black_code_style/index.rst +++ b/docs/the_black_code_style/index.rst @@ -9,9 +9,33 @@ The Black Code Style *Black* is a PEP 8 compliant opinionated formatter with its own style. -It should be noted that while keeping the style unchanged throughout releases is a -goal, the *Black* code style isn't set in stone. Sometimes it's modified in response to -user feedback or even changes to the Python language! +While keeping the style unchanged throughout releases has always been a goal, +the *Black* code style isn't set in stone. It evolves to accomodate for new features +in the Python language and, ocassionally, in response to user feedback. + +Stability Policy +---------------- + +The following policy applies for the *Black* code style, in non pre-release +versions of *Black*: + +- The same code, formatted with the same options, will produce the same + output for all releases in a given calendar year. + + This means projects can safely use `black ~= 22.0` without worrying about + major formatting changes disrupting their project in 2022. We may still + fix bugs where *Black* crashes on some code, and make other improvements + that do not affect formatting. + +- The first release in a new calendar year *may* contain formatting changes, + although these will be minimised as much as possible. This is to allow for + improved formatting enabled by newer Python language syntax as well as due + to improvements in the formatting logic. + +- The ``--future`` flag is exempt from this policy. There are no guarentees + around the stability of the output with that flag passed into *Black*. This + flag is intended for allowing experimentation with the proposed changes to + the *Black* code style. Documentation for both the current and future styles can be found: From da8a5bb1895e5434695d9dc2d844119cd8f88524 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Thu, 21 Oct 2021 19:38:39 -0700 Subject: [PATCH 017/700] Disallow any Generics on mypy except in black_primer (#2556) Only black_primer needs the disallowal - means we'll get better typing everywhere else. --- mypy.ini | 8 ++++++-- src/black/__init__.py | 10 +++++----- src/black_primer/lib.py | 2 +- tests/test_black.py | 4 +++- tests/test_primer.py | 6 ++++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/mypy.ini b/mypy.ini index 7e563e6f696..dabef458341 100644 --- a/mypy.ini +++ b/mypy.ini @@ -22,8 +22,7 @@ strict_optional=True warn_no_return=True warn_redundant_casts=True warn_unused_ignores=True -# Until we're not supporting 3.6 primer needs this -disallow_any_generics=False +disallow_any_generics=True # The following are off by default. Flip them on if you feel # adventurous. @@ -35,6 +34,11 @@ cache_dir=/dev/null [mypy-aiohttp.*] follow_imports=skip + +[mypy-black_primer.*] +# Until we're not supporting 3.6 primer needs this +disallow_any_generics=False + [mypy-black] # The following is because of `patch_click()`. Remove when # we drop Python 3.6 support. diff --git a/src/black/__init__.py b/src/black/__init__.py index fdbaf040d64..f01e449ea05 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -170,7 +170,7 @@ def validate_regex( ctx: click.Context, param: click.Parameter, value: Optional[str], -) -> Optional[Pattern]: +) -> Optional[Pattern[str]]: try: return re_compile_maybe_verbose(value) if value is not None else None except re.error: @@ -388,10 +388,10 @@ def main( quiet: bool, verbose: bool, required_version: str, - include: Pattern, - exclude: Optional[Pattern], - extend_exclude: Optional[Pattern], - force_exclude: Optional[Pattern], + include: Pattern[str], + exclude: Optional[Pattern[str]], + extend_exclude: Optional[Pattern[str]], + force_exclude: Optional[Pattern[str]], stdin_filename: Optional[str], workers: int, src: Tuple[str, ...], diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 7494ae6dc7d..c7842797485 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -258,7 +258,7 @@ async def git_checkout_or_rebase( def handle_PermissionError( - func: Callable, path: Path, exc: Tuple[Any, Any, Any] + func: Callable[..., None], path: Path, exc: Tuple[Any, Any, Any] ) -> None: """ Handle PermissionError during shutil.rmtree. diff --git a/tests/test_black.py b/tests/test_black.py index beb56cf1fdb..1fc63c942e9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2018,7 +2018,9 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: black_source_lines = _bf.readlines() -def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable: +def tracefunc( + frame: types.FrameType, event: str, arg: Any +) -> Callable[[types.FrameType, str, Any], Any]: """Show function calls `from black/__init__.py` as they happen. Register this with `sys.settrace()` in a test you're debugging. diff --git a/tests/test_primer.py b/tests/test_primer.py index e7f99fdb0c2..dc30a7a2244 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -11,7 +11,7 @@ from platform import system from subprocess import CalledProcessError from tempfile import TemporaryDirectory, gettempdir -from typing import Any, Callable, Generator, Iterator, Tuple +from typing import Any, Callable, Iterator, Tuple from unittest.mock import Mock, patch from click.testing import CliRunner @@ -44,7 +44,9 @@ @contextmanager -def capture_stdout(command: Callable, *args: Any, **kwargs: Any) -> Generator: +def capture_stdout( + command: Callable[..., Any], *args: Any, **kwargs: Any +) -> Iterator[str]: old_stdout, sys.stdout = sys.stdout, StringIO() try: command(*args, **kwargs) From 62ed5389fca51384721245ff1c2f1c62a13a04ff Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Thu, 21 Oct 2021 20:59:48 -0700 Subject: [PATCH 018/700] Remove some unneeded exceptions from mypy.ini (#2557) --- mypy.ini | 8 -------- src/black/__init__.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/mypy.ini b/mypy.ini index dabef458341..6e8b7906e1d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -32,14 +32,6 @@ check_untyped_defs=True # No incremental mode cache_dir=/dev/null -[mypy-aiohttp.*] -follow_imports=skip - [mypy-black_primer.*] # Until we're not supporting 3.6 primer needs this disallow_any_generics=False - -[mypy-black] -# The following is because of `patch_click()`. Remove when -# we drop Python 3.6 support. -warn_unused_ignores=False diff --git a/src/black/__init__.py b/src/black/__init__.py index f01e449ea05..5c6cb672aa2 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1287,7 +1287,7 @@ def patch_click() -> None: """ try: from click import core - from click import _unicodefun # type: ignore + from click import _unicodefun except ModuleNotFoundError: return From 26970742b76ddfd921fa36e4005738048477c952 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Wed, 27 Oct 2021 15:36:10 +0100 Subject: [PATCH 019/700] Refactor Jupyter magic handling (#2545) --- src/black/handle_ipynb_magics.py | 72 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 63c8aafe35b..f10eaed4f3e 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -39,18 +39,18 @@ ) NON_PYTHON_CELL_MAGICS = frozenset( ( - "%%bash", - "%%html", - "%%javascript", - "%%js", - "%%latex", - "%%markdown", - "%%perl", - "%%ruby", - "%%script", - "%%sh", - "%%svg", - "%%writefile", + "bash", + "html", + "javascript", + "js", + "latex", + "markdown", + "perl", + "ruby", + "script", + "sh", + "svg", + "writefile", ) ) TOKEN_HEX = secrets.token_hex @@ -230,10 +230,11 @@ def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: cell_magic_finder.visit(tree) if cell_magic_finder.cell_magic is None: return src, replacements - if cell_magic_finder.cell_magic.header.split()[0] in NON_PYTHON_CELL_MAGICS: + if cell_magic_finder.cell_magic.name in NON_PYTHON_CELL_MAGICS: raise NothingChanged - mask = get_token(src, cell_magic_finder.cell_magic.header) - replacements.append(Replacement(mask=mask, src=cell_magic_finder.cell_magic.header)) + header = cell_magic_finder.cell_magic.header + mask = get_token(src, header) + replacements.append(Replacement(mask=mask, src=header)) return f"{mask}\n{cell_magic_finder.cell_magic.body}", replacements @@ -311,11 +312,26 @@ def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]: ) +def _get_str_args(args: List[ast.expr]) -> List[str]: + str_args = [] + for arg in args: + assert isinstance(arg, ast.Str) + str_args.append(arg.s) + return str_args + + @dataclasses.dataclass(frozen=True) class CellMagic: - header: str + name: str + params: Optional[str] body: str + @property + def header(self) -> str: + if self.params: + return f"%%{self.name} {self.params}" + return f"%%{self.name}" + @dataclasses.dataclass class CellMagicFinder(ast.NodeVisitor): @@ -345,14 +361,8 @@ def visit_Expr(self, node: ast.Expr) -> None: and _is_ipython_magic(node.value.func) and node.value.func.attr == "run_cell_magic" ): - args = [] - for arg in node.value.args: - assert isinstance(arg, ast.Str) - args.append(arg.s) - header = f"%%{args[0]}" - if args[1]: - header += f" {args[1]}" - self.cell_magic = CellMagic(header=header, body=args[2]) + args = _get_str_args(node.value.args) + self.cell_magic = CellMagic(name=args[0], params=args[1], body=args[2]) self.generic_visit(node) @@ -404,12 +414,8 @@ def visit_Assign(self, node: ast.Assign) -> None: and _is_ipython_magic(node.value.func) and node.value.func.attr == "getoutput" ): - args = [] - for arg in node.value.args: - assert isinstance(arg, ast.Str) - args.append(arg.s) - assert args - src = f"!{args[0]}" + (arg,) = _get_str_args(node.value.args) + src = f"!{arg}" self.magics[node.value.lineno].append( OffsetAndMagic(node.value.col_offset, src) ) @@ -435,11 +441,7 @@ def visit_Expr(self, node: ast.Expr) -> None: and we look for instances of any of the latter. """ if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func): - args = [] - for arg in node.value.args: - assert isinstance(arg, ast.Str) - args.append(arg.s) - assert args + args = _get_str_args(node.value.args) if node.value.func.attr == "run_line_magic": if args[0] == "pinfo": src = f"?{args[1]}" From aedb4ff7f061b321ea5804bc4fc4943c52c6a786 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Wed, 27 Oct 2021 07:37:20 -0700 Subject: [PATCH 020/700] Print out line diff on test failure (#2552) It currently prints both ASTs - this also adds the line diff, making it much easier to visualize the changes as well. Not too verbose since it's only a diff. --- tests/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index 84e98bb0fbd..8755111f7c5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -9,7 +9,7 @@ import black from black.debug import DebugVisitor from black.mode import TargetVersion -from black.output import err, out +from black.output import diff, err, out THIS_DIR = Path(__file__).parent DATA_DIR = THIS_DIR / "data" @@ -47,6 +47,9 @@ def _assert_format_equal(expected: str, actual: str) -> None: except Exception as ve: err(str(ve)) + if actual != expected: + out(diff(expected, actual, "expected", "actual")) + assert actual == expected From 467efe15562e3bad88b1eb3bc11f76b5b9a68816 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Wed, 27 Oct 2021 11:31:34 -0700 Subject: [PATCH 021/700] Add --projects cli flag to black-primer (#2555) * Add --projects cli flag to black-primer Makes it possible to run a subset of projects on black primer * Refactor into click callback --- CHANGES.md | 1 + mypy.ini | 4 ++++ src/black_primer/cli.py | 43 +++++++++++++++++++++++++++++++++- src/black_primer/lib.py | 11 +++++---- tests/test_format.py | 2 ++ tests/test_primer.py | 51 +++++++++++++++++++++++++++++++++++++++-- 6 files changed, 104 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2a3a60f82c6..a8307ee61ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Add new `--workers` parameter (#2514) - Fixed feature detection for positional-only arguments in lambdas (#2532) - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) +- Add primer support for --projects (#2555) ### _Blackd_ diff --git a/mypy.ini b/mypy.ini index 6e8b7906e1d..62c1c7fefaa 100644 --- a/mypy.ini +++ b/mypy.ini @@ -35,3 +35,7 @@ cache_dir=/dev/null [mypy-black_primer.*] # Until we're not supporting 3.6 primer needs this disallow_any_generics=False + +[mypy-tests.test_primer] +# Until we're not supporting 3.6 primer needs this +disallow_any_generics=False diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index 8360fc3c703..2395d35886a 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -1,13 +1,14 @@ # coding=utf8 import asyncio +import json import logging import sys from datetime import datetime from pathlib import Path from shutil import rmtree, which from tempfile import gettempdir -from typing import Any, Union, Optional +from typing import Any, List, Optional, Union import click @@ -42,12 +43,42 @@ def _handle_debug( return debug +def load_projects(config_path: Path) -> List[str]: + with open(config_path) as config: + return sorted(json.load(config)["projects"].keys()) + + +# Unfortunately does import time file IO - but appears to be the only +# way to get `black-primer --help` to show projects list +DEFAULT_PROJECTS = load_projects(DEFAULT_CONFIG) + + +def _projects_callback( + ctx: click.core.Context, + param: Optional[Union[click.core.Option, click.core.Parameter]], + projects: str, +) -> List[str]: + requested_projects = set(projects.split(",")) + available_projects = set( + DEFAULT_PROJECTS + if str(DEFAULT_CONFIG) == ctx.params["config"] + else load_projects(ctx.params["config"]) + ) + + unavailable = requested_projects - available_projects + if unavailable: + LOG.error(f"Projects not found: {unavailable}. Available: {available_projects}") + + return sorted(requested_projects & available_projects) + + async def async_main( config: str, debug: bool, keep: bool, long_checkouts: bool, no_diff: bool, + projects: List[str], rebase: bool, workdir: str, workers: int, @@ -66,6 +97,7 @@ async def async_main( config, work_path, workers, + projects, keep, long_checkouts, rebase, @@ -88,6 +120,8 @@ async def async_main( type=click.Path(exists=True), show_default=True, help="JSON config file path", + # Eager - because config path is used by other callback options + is_eager=True, ) @click.option( "--debug", @@ -116,6 +150,13 @@ async def async_main( show_default=True, help="Disable showing source file changes in black output", ) +@click.option( + "--projects", + default=",".join(DEFAULT_PROJECTS), + callback=_projects_callback, + show_default=True, + help="Comma separated list of projects to run", +) @click.option( "-R", "--rebase", diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index c7842797485..351501673f8 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -283,16 +283,16 @@ def handle_PermissionError( async def load_projects_queue( config_path: Path, + projects_to_run: List[str], ) -> Tuple[Dict[str, Any], asyncio.Queue]: """Load project config and fill queue with all the project names""" with config_path.open("r") as cfp: config = json.load(cfp) # TODO: Offer more options here - # e.g. Run on X random packages or specific sub list etc. - project_names = sorted(config["projects"].keys()) - queue: asyncio.Queue = asyncio.Queue(maxsize=len(project_names)) - for project in project_names: + # e.g. Run on X random packages etc. + queue: asyncio.Queue = asyncio.Queue(maxsize=len(projects_to_run)) + for project in projects_to_run: await queue.put(project) return config, queue @@ -365,6 +365,7 @@ async def process_queue( config_file: str, work_path: Path, workers: int, + projects_to_run: List[str], keep: bool = False, long_checkouts: bool = False, rebase: bool = False, @@ -383,7 +384,7 @@ async def process_queue( results.stats["success"] = 0 results.stats["wrong_py_ver"] = 0 - config, queue = await load_projects_queue(Path(config_file)) + config, queue = await load_projects_queue(Path(config_file), projects_to_run) project_count = queue.qsize() s = "" if project_count == 1 else "s" LOG.info(f"{project_count} project{s} to run Black over") diff --git a/tests/test_format.py b/tests/test_format.py index a659382092a..649c1572bee 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -93,6 +93,8 @@ "src/black/strings.py", "src/black/trans.py", "src/blackd/__init__.py", + "src/black_primer/cli.py", + "src/black_primer/lib.py", "src/blib2to3/pygram.py", "src/blib2to3/pytree.py", "src/blib2to3/pgen2/conv.py", diff --git a/tests/test_primer.py b/tests/test_primer.py index dc30a7a2244..8d00d8353a7 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -11,7 +11,7 @@ from platform import system from subprocess import CalledProcessError from tempfile import TemporaryDirectory, gettempdir -from typing import Any, Callable, Iterator, Tuple +from typing import Any, Callable, Iterator, List, Tuple, TypeVar from unittest.mock import Mock, patch from click.testing import CliRunner @@ -89,6 +89,24 @@ async def return_zero(*args: Any, **kwargs: Any) -> int: return 0 +if sys.version_info >= (3, 9): + T = TypeVar("T") + Q = asyncio.Queue[T] +else: + T = Any + Q = asyncio.Queue + + +def collect(queue: Q) -> List[T]: + ret = [] + while True: + try: + item = queue.get_nowait() + ret.append(item) + except asyncio.QueueEmpty: + return ret + + class PrimerLibTests(unittest.TestCase): def test_analyze_results(self) -> None: fake_results = lib.Results( @@ -198,10 +216,25 @@ def test_process_queue(self, mock_stdout: Mock) -> None: with patch("black_primer.lib.git_checkout_or_rebase", return_false): with TemporaryDirectory() as td: return_val = loop.run_until_complete( - lib.process_queue(str(config_path), Path(td), 2) + lib.process_queue( + str(config_path), Path(td), 2, ["django", "pyramid"] + ) ) self.assertEqual(0, return_val) + @event_loop() + def test_load_projects_queue(self) -> None: + """Test the process queue on primer itself + - If you have non black conforming formatting in primer itself this can fail""" + loop = asyncio.get_event_loop() + config_path = Path(lib.__file__).parent / "primer.json" + + config, projects_queue = loop.run_until_complete( + lib.load_projects_queue(config_path, ["django", "pyramid"]) + ) + projects = collect(projects_queue) + self.assertEqual(projects, ["django", "pyramid"]) + class PrimerCLITests(unittest.TestCase): @event_loop() @@ -217,6 +250,7 @@ def test_async_main(self) -> None: "workdir": str(work_dir), "workers": 69, "no_diff": False, + "projects": "", } with patch("black_primer.cli.lib.process_queue", return_zero): return_val = loop.run_until_complete(cli.async_main(**args)) # type: ignore @@ -230,6 +264,19 @@ def test_help_output(self) -> None: result = runner.invoke(cli.main, ["--help"]) self.assertEqual(result.exit_code, 0) + def test_projects(self) -> None: + runner = CliRunner() + with event_loop(): + result = runner.invoke(cli.main, ["--projects=tox,asdf"]) + self.assertEqual(result.exit_code, 0) + assert "1 / 1 succeeded" in result.output + + with event_loop(): + runner = CliRunner() + result = runner.invoke(cli.main, ["--projects=tox,attrs"]) + self.assertEqual(result.exit_code, 0) + assert "2 / 2 succeeded" in result.output + if __name__ == "__main__": unittest.main() From 5434407af7ba262f74d272c738006cbf1d0ab11a Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Thu, 28 Oct 2021 10:35:37 -0700 Subject: [PATCH 022/700] black-primer: Print summary after individual failures (#2570) If the individual failures are verbose, it's useful to have the summary at the end. Otherwise, it can be really difficult to figure out which projects have an issue. --- CHANGES.md | 6 +++++- src/black_primer/lib.py | 24 ++++++++++++++---------- tests/test_primer.py | 15 +++++++++------ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a8307ee61ec..76a7ca6fd3d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,13 +8,17 @@ - Add new `--workers` parameter (#2514) - Fixed feature detection for positional-only arguments in lambdas (#2532) - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) -- Add primer support for --projects (#2555) ### _Blackd_ - Remove dependency on aiohttp-cors (#2500) - Bump required aiohttp version to 3.7.4 (#2509) +### _Black-Primer_ + +- Add primer support for --projects (#2555) +- Print primer summary after individual failures (#2570) + ### Integrations - Allow to pass `target_version` in the vim plugin (#1319) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 351501673f8..13724f431ce 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -88,6 +88,18 @@ def analyze_results(project_count: int, results: Results) -> int: failed_pct = round(((results.stats["failed"] / project_count) * 100), 2) success_pct = round(((results.stats["success"] / project_count) * 100), 2) + if results.failed_projects: + click.secho("\nFailed projects:\n", bold=True) + + for project_name, project_cpe in results.failed_projects.items(): + print(f"## {project_name}:") + print(f" - Returned {project_cpe.returncode}") + if project_cpe.stderr: + print(f" - stderr:\n{project_cpe.stderr.decode('utf8')}") + if project_cpe.stdout: + print(f" - stdout:\n{project_cpe.stdout.decode('utf8')}") + print("") + click.secho("-- primer results 📊 --\n", bold=True) click.secho( f"{results.stats['success']} / {project_count} succeeded ({success_pct}%) ✅", @@ -110,16 +122,8 @@ def analyze_results(project_count: int, results: Results) -> int: ) if results.failed_projects: - click.secho("\nFailed projects:\n", bold=True) - - for project_name, project_cpe in results.failed_projects.items(): - print(f"## {project_name}:") - print(f" - Returned {project_cpe.returncode}") - if project_cpe.stderr: - print(f" - stderr:\n{project_cpe.stderr.decode('utf8')}") - if project_cpe.stdout: - print(f" - stdout:\n{project_cpe.stdout.decode('utf8')}") - print("") + failed = ", ".join(results.failed_projects.keys()) + click.secho(f"\nFailed projects: {failed}\n", bold=True) return results.stats["failed"] diff --git a/tests/test_primer.py b/tests/test_primer.py index 8d00d8353a7..2aac2d09ff5 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -20,6 +20,14 @@ EXPECTED_ANALYSIS_OUTPUT = """\ + +Failed projects: + +## black: + - Returned 69 + - stdout: +Black didn't work + -- primer results 📊 -- 68 / 69 succeeded (98.55%) ✅ @@ -28,12 +36,7 @@ - 0 projects skipped due to Python version - 0 skipped due to long checkout -Failed projects: - -## black: - - Returned 69 - - stdout: -Black didn't work +Failed projects: black """ FAKE_PROJECT_CONFIG = { From cbf5401efff0524f7395c5fb81551de75b17c89e Mon Sep 17 00:00:00 2001 From: dawn <78233879+dawnofmidnight@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:50:45 -0400 Subject: [PATCH 023/700] fix: allow tests to be run from (hopefully) any directory (GH-2574) * fix: allow tests to be run from the tests/ directory * fix: try fixing windows build with MarcoGorelli's suggestion * Windows hotfix + better respect test's spirit Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- tests/test_black.py | 9 ++++++--- tests/test_ipynb.py | 49 +++++++++++++++++---------------------------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 1fc63c942e9..5647a00e48b 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -50,6 +50,7 @@ DATA_DIR, DEFAULT_MODE, DETERMINISTIC_HEADER, + PROJECT_ROOT, PY36_VERSIONS, THIS_DIR, BlackBaseTestCase, @@ -1512,9 +1513,11 @@ def test_code_option_config(self) -> None: """ with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: args = ["--code", "print"] - CliRunner().invoke(black.main, args) + # This is the only directory known to contain a pyproject.toml + with change_directory(PROJECT_ROOT): + CliRunner().invoke(black.main, args) + pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve() - pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve() assert ( len(parse.mock_calls) >= 1 ), "Expected config parse to be called with the current directory." @@ -1529,7 +1532,7 @@ def test_code_option_parent_config(self) -> None: Test that the code option finds the pyproject.toml in the parent directory. """ with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: - with change_directory(Path("tests")): + with change_directory(THIS_DIR): args = ["--code", "print"] CliRunner().invoke(black.main, args) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 12f176c9341..ba460074e9a 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -1,4 +1,5 @@ -import pathlib +import re + from click.testing import CliRunner from black.handle_ipynb_magics import jupyter_dependencies_are_installed from black import ( @@ -8,11 +9,11 @@ format_file_contents, format_file_in_place, ) -import os import pytest from black import Mode from _pytest.monkeypatch import MonkeyPatch from py.path import local +from tests.util import DATA_DIR pytestmark = pytest.mark.jupyter pytest.importorskip("IPython", reason="IPython is an optional dependency") @@ -178,9 +179,7 @@ def test_empty_cell() -> None: def test_entire_notebook_empty_metadata() -> None: - with open( - os.path.join("tests", "data", "notebook_empty_metadata.ipynb"), "rb" - ) as fd: + with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd: content_bytes = fd.read() content = content_bytes.decode() result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) @@ -217,9 +216,7 @@ def test_entire_notebook_empty_metadata() -> None: def test_entire_notebook_trailing_newline() -> None: - with open( - os.path.join("tests", "data", "notebook_trailing_newline.ipynb"), "rb" - ) as fd: + with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd: content_bytes = fd.read() content = content_bytes.decode() result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) @@ -268,9 +265,7 @@ def test_entire_notebook_trailing_newline() -> None: def test_entire_notebook_no_trailing_newline() -> None: - with open( - os.path.join("tests", "data", "notebook_no_trailing_newline.ipynb"), "rb" - ) as fd: + with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd: content_bytes = fd.read() content = content_bytes.decode() result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) @@ -319,9 +314,7 @@ def test_entire_notebook_no_trailing_newline() -> None: def test_entire_notebook_without_changes() -> None: - with open( - os.path.join("tests", "data", "notebook_without_changes.ipynb"), "rb" - ) as fd: + with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd: content_bytes = fd.read() content = content_bytes.decode() with pytest.raises(NothingChanged): @@ -329,7 +322,7 @@ def test_entire_notebook_without_changes() -> None: def test_non_python_notebook() -> None: - with open(os.path.join("tests", "data", "non_python_notebook.ipynb"), "rb") as fd: + with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd: content_bytes = fd.read() content = content_bytes.decode() with pytest.raises(NothingChanged): @@ -342,23 +335,17 @@ def test_empty_string() -> None: def test_unparseable_notebook() -> None: - msg = ( - r"File 'tests[/\\]data[/\\]notebook_which_cant_be_parsed\.ipynb' " - r"cannot be parsed as valid Jupyter notebook\." - ) + path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb" + msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\." with pytest.raises(ValueError, match=msg): - format_file_in_place( - pathlib.Path("tests") / "data/notebook_which_cant_be_parsed.ipynb", - fast=True, - mode=JUPYTER_MODE, - ) + format_file_in_place(path, fast=True, mode=JUPYTER_MODE) def test_ipynb_diff_with_change() -> None: result = runner.invoke( main, [ - os.path.join("tests", "data", "notebook_trailing_newline.ipynb"), + str(DATA_DIR / "notebook_trailing_newline.ipynb"), "--diff", ], ) @@ -370,7 +357,7 @@ def test_ipynb_diff_with_no_change() -> None: result = runner.invoke( main, [ - os.path.join("tests", "data", "notebook_without_changes.ipynb"), + str(DATA_DIR / "notebook_without_changes.ipynb"), "--diff", ], ) @@ -383,7 +370,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_single( ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() - nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + nb = DATA_DIR / "notebook_trailing_newline.ipynb" tmp_nb = tmpdir / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -405,7 +392,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() - nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + nb = DATA_DIR / "notebook_trailing_newline.ipynb" tmp_nb = tmpdir / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -423,7 +410,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( def test_ipynb_flag(tmpdir: local) -> None: - nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + nb = DATA_DIR / "notebook_trailing_newline.ipynb" tmp_nb = tmpdir / "notebook.a_file_extension_which_is_definitely_not_ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -440,11 +427,11 @@ def test_ipynb_flag(tmpdir: local) -> None: def test_ipynb_and_pyi_flags() -> None: - nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + nb = DATA_DIR / "notebook_trailing_newline.ipynb" result = runner.invoke( main, [ - nb, + str(nb), "--pyi", "--ipynb", "--diff", From 6b38b52187a7a7b9fedcf257c5ba45cc16f2e8a2 Mon Sep 17 00:00:00 2001 From: Roma <81729714+rtdev-com@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:45:09 -0700 Subject: [PATCH 024/700] Add Tesla to organizations list (#2577) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bf0ed8d16f..f9061c33863 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Ked many more. The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo, -QuantumBlack. +QuantumBlack, Tesla. Are we missing anyone? Let us know. From 92eeacc2e3ce917d6364ec06d891436f11536a1c Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Sat, 30 Oct 2021 11:54:43 -0700 Subject: [PATCH 025/700] Use STDIN project in test_projects to ensure it runs quickly (#2575) Existing test was actually running a full black-primer run which could be slow. This goes from 8 seconds to 0.4 seconds on my machine. Needed to move to top level scope to leverage the caplog feature of pytest in order to test that the command line was parsing the bogus arguments and dumping to stderr. --- tests/test_primer.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/test_primer.py b/tests/test_primer.py index 2aac2d09ff5..9bb401574ca 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -9,6 +9,7 @@ from os import getpid from pathlib import Path from platform import system +from pytest import LogCaptureFixture from subprocess import CalledProcessError from tempfile import TemporaryDirectory, gettempdir from typing import Any, Callable, Iterator, List, Tuple, TypeVar @@ -267,18 +268,23 @@ def test_help_output(self) -> None: result = runner.invoke(cli.main, ["--help"]) self.assertEqual(result.exit_code, 0) - def test_projects(self) -> None: + +def test_projects(caplog: LogCaptureFixture) -> None: + with event_loop(): + runner = CliRunner() + result = runner.invoke(cli.main, ["--projects=STDIN,asdf"]) + assert result.exit_code == 0 + assert "1 / 1 succeeded" in result.output + assert "Projects not found: {'asdf'}" in caplog.text + + caplog.clear() + + with event_loop(): runner = CliRunner() - with event_loop(): - result = runner.invoke(cli.main, ["--projects=tox,asdf"]) - self.assertEqual(result.exit_code, 0) - assert "1 / 1 succeeded" in result.output - - with event_loop(): - runner = CliRunner() - result = runner.invoke(cli.main, ["--projects=tox,attrs"]) - self.assertEqual(result.exit_code, 0) - assert "2 / 2 succeeded" in result.output + result = runner.invoke(cli.main, ["--projects=fdsa,STDIN"]) + assert result.exit_code == 0 + assert "1 / 1 succeeded" in result.output + assert "Projects not found: {'fdsa'}" in caplog.text if __name__ == "__main__": From fac498bf31af509e0f62bf61459014c9a4e094a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Sun, 31 Oct 2021 01:01:40 +0300 Subject: [PATCH 026/700] Update bug template (#2538) --- .github/ISSUE_TEMPLATE/bug_report.md | 59 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e8d232c8b34..cb64cf9325d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,41 +6,58 @@ labels: "T: bug" assignees: "" --- + + **Describe the bug** **To Reproduce** - -For example: -1. Take this file '...' -1. Run _Black_ on it with these arguments '...' -1. See error --> +For example, take this code: -**Expected behavior** +```python +this = "code" +``` - +And run it with these arguments: -**Environment (please complete the following information):** +```sh +$ black file.py --target-version py39 +``` -- Version: -- OS and Python version: +The resulting error is: -**Does this bug also happen on main?** +> cannot format file.py: INTERNAL ERROR: ... - + + +**Environment** + + + +- Black's version: +- OS and Python version: **Additional context** From 9afffacaa0e5ac911f9feacb916bc48473dcb117 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 30 Oct 2021 18:35:55 -0400 Subject: [PATCH 027/700] Address mypy errors on 3.10 w/ asyncio loop parameter (#2580) --- CHANGES.md | 2 ++ src/black/__init__.py | 5 ++++- src/black/concurrency.py | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 76a7ca6fd3d..c49516c9081 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ - Add new `--workers` parameter (#2514) - Fixed feature detection for positional-only arguments in lambdas (#2532) - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) +- Fixed a Python 3.10 compatibility issue where the loop argument was still being passed + even though it has been removed (#2580) ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 5c6cb672aa2..c503c1a55f7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -763,7 +763,10 @@ async def schedule_formatting( sources_to_cache.append(src) report.done(src, changed) if cancelled: - await asyncio.gather(*cancelled, loop=loop, return_exceptions=True) + if sys.version_info >= (3, 7): + await asyncio.gather(*cancelled, return_exceptions=True) + else: + await asyncio.gather(*cancelled, loop=loop, return_exceptions=True) if sources_to_cache: write_cache(cache, sources_to_cache, mode) diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 69d79f534e8..24f67b62f06 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -42,9 +42,12 @@ def shutdown(loop: asyncio.AbstractEventLoop) -> None: for task in to_cancel: task.cancel() - loop.run_until_complete( - asyncio.gather(*to_cancel, loop=loop, return_exceptions=True) - ) + if sys.version_info >= (3, 7): + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + else: + loop.run_until_complete( + asyncio.gather(*to_cancel, loop=loop, return_exceptions=True) + ) finally: # `concurrent.futures.Future` objects cannot be cancelled once they # are already running. There might be some when the `shutdown()` happened. From 7bf233a9446a7611b22bc2f73f7e221886632725 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 31 Oct 2021 10:41:12 -0700 Subject: [PATCH 028/700] Pin regex in docker to 2021.10.8 (GH-2579) * Pin regex in docker to 2021.10.8 - This is due to 2021.10.8 having arm wheels and newer versions not I will go see if I can help restore arm build @ https://bitbucket.org/mrabarnett/mrab-regex/issues/399/missing-wheel-for-macosx-and-the-new-m1 soon. Test: Build on my M1 mac: `docker build -t cooperlees/black .` * Add in that the pin is only for docker --- CHANGES.md | 2 ++ Dockerfile | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c49516c9081..4c04eccde48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,8 @@ ### Integrations - Allow to pass `target_version` in the vim plugin (#1319) +- Pin regex module to 2021.10.8 in our docker file as it has arm wheels available + (#2579) ## 21.9b0 diff --git a/Dockerfile b/Dockerfile index 9542479eca5..ce88f0ce5e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ FROM python:3-slim +# TODO: Remove regex version pin once we get newer arm wheels RUN mkdir /src COPY . /src/ RUN pip install --no-cache-dir --upgrade pip setuptools wheel \ && apt update && apt install -y git \ && cd /src \ + && pip install --no-cache-dir regex==2021.10.8 \ && pip install --no-cache-dir .[colorama,d] \ && rm -rf /src \ && apt remove -y git \ From b21c0c3d28d87bc944a1fdc979b30a0707b0df89 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 31 Oct 2021 19:46:12 -0400 Subject: [PATCH 029/700] Deprecate Python 2 formatting support (#2523) * Prepare for Python 2 depreciation - Use BlackRunner and .stdout in command line test So the next commit won't break this test. This is in its own commit so we can just revert the depreciation commit when dropping Python 2 support completely. * Deprecate Python 2 formatting support --- CHANGES.md | 1 + docs/faq.md | 10 +++++++--- src/black/__init__.py | 17 ++++++++++++++++- src/black/mode.py | 10 +++++++++- src/blib2to3/pgen2/token.py | 3 +++ tests/test_black.py | 19 +++++++++++++++++-- 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4c04eccde48..9990d8cf459 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) - Fixed a Python 3.10 compatibility issue where the loop argument was still being passed even though it has been removed (#2580) +- Deprecate Python 2 formatting support (#2523) ### _Blackd_ diff --git a/docs/faq.md b/docs/faq.md index 9fe53922b6d..77f9df51fd4 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -74,13 +74,17 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? +```{warning} +Python 2 support has been deprecated since 21.10b0. + +This support will be dropped in the first stable release, expected for January 2022. +See [The Black Code Style](the_black_code_style/index.rst) for details. +``` + For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra to format Python 2 files too! In terms of running _Black_ though, Python 3.6 or newer is required. -Note that this support will be dropped in the first stable release, expected for -January 2022. See [The Black Code Style](the_black_code_style/index.rst) for details. - ## Why does my linter or typechecker complain after I format my code? Some linters and other tools use magical comments (e.g., `# noqa`, `# type: ignore`) to diff --git a/src/black/__init__.py b/src/black/__init__.py index c503c1a55f7..831cda934a8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1061,6 +1061,15 @@ def f( versions = mode.target_versions else: versions = detect_target_versions(src_node) + + # TODO: fully drop support and this code hopefully in January 2022 :D + if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}: + msg = ( + "DEPRECATION: Python 2 support will be removed in the first stable release" + "expected in January 2022." + ) + err(msg, fg="yellow", bold=True) + normalize_fmt_off(src_node) lines = LineGenerator( mode=mode, @@ -1103,7 +1112,7 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: return tiow.read(), encoding, newline -def get_features_used(node: Node) -> Set[Feature]: +def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 """Return a set of (relatively) new Python features used in this file. Currently looking for: @@ -1113,6 +1122,7 @@ def get_features_used(node: Node) -> Set[Feature]: - positional only arguments in function signatures and lambdas; - assignment expression; - relaxed decorator syntax; + - print / exec statements; """ features: Set[Feature] = set() for n in node.pre_order(): @@ -1161,6 +1171,11 @@ def get_features_used(node: Node) -> Set[Feature]: if argch.type in STARS: features.add(feature) + elif n.type == token.PRINT_STMT: + features.add(Feature.PRINT_STMT) + elif n.type == token.EXEC_STMT: + features.add(Feature.EXEC_STMT) + return features diff --git a/src/black/mode.py b/src/black/mode.py index 0b7624eaf8a..374c47a42eb 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -41,9 +41,17 @@ class Feature(Enum): RELAXED_DECORATORS = 10 FORCE_OPTIONAL_PARENTHESES = 50 + # temporary for Python 2 deprecation + PRINT_STMT = 200 + EXEC_STMT = 201 + VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { - TargetVersion.PY27: {Feature.ASYNC_IDENTIFIERS}, + TargetVersion.PY27: { + Feature.ASYNC_IDENTIFIERS, + Feature.PRINT_STMT, + Feature.EXEC_STMT, + }, TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, TargetVersion.PY35: { diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index 1e0dec9c714..349ba8023a2 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -74,6 +74,9 @@ COLONEQUAL: Final = 59 N_TOKENS: Final = 60 NT_OFFSET: Final = 256 +# temporary for Python 2 deprecation +PRINT_STMT: Final = 316 +EXEC_STMT: Final = 288 # --end constants-- tok_name: Final[Dict[int, str]] = {} diff --git a/tests/test_black.py b/tests/test_black.py index 5647a00e48b..b96a5438557 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1401,14 +1401,14 @@ def test_docstring_reformat_for_py27(self) -> None: ) expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n' - result = CliRunner().invoke( + result = BlackRunner().invoke( black.main, ["-", "-q", "--target-version=py27"], input=BytesIO(source), ) self.assertEqual(result.exit_code, 0) - actual = result.output + actual = result.stdout self.assertFormatEqual(actual, expected) @staticmethod @@ -2017,6 +2017,21 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: ) +@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"]) +def test_python_2_deprecation_with_target_version(explicit: bool) -> None: + args = [ + "--config", + str(THIS_DIR / "empty.toml"), + str(DATA_DIR / "python2.py"), + "--check", + ] + if explicit: + args.append("--target-version=py27") + with cache_dir(): + result = BlackRunner().invoke(black.main, args) + assert "DEPRECATION: Python 2 support will be removed" in result.stderr + + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From bd961304b63ad8178610df7242edf180d825e336 Mon Sep 17 00:00:00 2001 From: Vincent Barbaresi Date: Mon, 1 Nov 2021 01:43:34 +0100 Subject: [PATCH 030/700] install build-essential to compile dependencies and use multi-stage build (#2582) - Install build-essential to avoid build issues like #2568 when dependencies don't have prebuilt wheels available - Use multi-stage build instead of trying to purge packages and cache from the image Copying `/root/.local/` installs only black's built Python dependencies (< 20 MB). So the image is barely larger than python:3-slim base image --- CHANGES.md | 4 ++-- Dockerfile | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9990d8cf459..aa40d87ab71 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,8 +25,8 @@ ### Integrations - Allow to pass `target_version` in the vim plugin (#1319) -- Pin regex module to 2021.10.8 in our docker file as it has arm wheels available - (#2579) +- Install build tools in docker file and use multi-stage build to keep the image size + down (#2582) ## 21.9b0 diff --git a/Dockerfile b/Dockerfile index ce88f0ce5e3..c393e29f632 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,17 @@ -FROM python:3-slim +FROM python:3-slim AS builder -# TODO: Remove regex version pin once we get newer arm wheels RUN mkdir /src COPY . /src/ RUN pip install --no-cache-dir --upgrade pip setuptools wheel \ - && apt update && apt install -y git \ + # Install build tools to compile dependencies that don't have prebuilt wheels + && apt update && apt install -y git build-essential \ && cd /src \ - && pip install --no-cache-dir regex==2021.10.8 \ - && pip install --no-cache-dir .[colorama,d] \ - && rm -rf /src \ - && apt remove -y git \ - && apt autoremove -y \ - && rm -rf /var/lib/apt/lists/* + && pip install --user --no-cache-dir .[colorama,d] + +FROM python:3-slim + +# copy only Python packages to limit the image size +COPY --from=builder /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH CMD ["black"] From 64c8be01f0cfedc94cb1c9ebd342ea77cafbb78a Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 31 Oct 2021 17:59:39 -0700 Subject: [PATCH 031/700] Update CHANGES.md for 21.10b0 release (#2583) * Update CHANGES.md for 21.10b0 release * Update version in docs/usage_and_configuration/the_basics.md * Also update docs/integrations/source_version_control.md ... --- 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 aa40d87ab71..b454a73edab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.10b0 ### _Black_ diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 6a1aa363d2b..cf0ef1dfed9 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.9b0 + rev: 21.10b0 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 49268b44f7c..533c213a4da 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.9b0 +black, version 21.10b0 ``` An option to require a specific version to be running is also provided. From f80f49767cacafaeb20016d483b8315474504e6a Mon Sep 17 00:00:00 2001 From: LordOfPolls <22540825+LordOfPolls@users.noreply.github.com> Date: Sat, 6 Nov 2021 16:04:27 +0000 Subject: [PATCH 032/700] Add a missing space in Python 2 deprecation (GH-2590) `DEPRECATION: Python 2 support will be removed in the first stable releaseexpected in January 2022` - > `DEPRECATION: Python 2 support will be removed in the first stable release expected in January 2022` --- 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 831cda934a8..ba4d3dea70e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1065,7 +1065,7 @@ def f( # TODO: fully drop support and this code hopefully in January 2022 :D if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}: msg = ( - "DEPRECATION: Python 2 support will be removed in the first stable release" + "DEPRECATION: Python 2 support will be removed in the first stable release " "expected in January 2022." ) err(msg, fg="yellow", bold=True) From f297c4644ee561ceadfb0bf3a08a89ac92b5a2ee Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 11 Nov 2021 17:52:13 -0500 Subject: [PATCH 033/700] primer: Hypothesis now requires Python>=3.8 (GH-2602) looks like their project dev tooling uses some newer syntax or something --- .gitignore | 7 ++++++- src/black_primer/primer.json | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f81bce8fd4e..249499b135e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,18 @@ _build docs/_static/pypi.svg .tox __pycache__ + +# Packaging artifacts black.egg-info +black.dist-info build/ dist/ pip-wheel-metadata/ +.eggs + src/_black_version.py .idea -.eggs + .dmypy.json *.swp .hypothesis/ diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 0d1018fc50e..2290d1df005 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -77,7 +77,7 @@ "expect_formatting_changes": true, "git_clone_url": "https://github.com/django/django.git", "long_checkout": false, - "py_versions": ["3.8", "3.9"] + "py_versions": ["3.8", "3.9", "3.10"] }, "flake8-bugbear": { "cli_arguments": ["--experimental-string-processing"], @@ -91,7 +91,7 @@ "expect_formatting_changes": true, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, - "py_versions": ["all"] + "py_versions": ["3.8", "3.9", "3.10"] }, "pandas": { "cli_arguments": ["--experimental-string-processing"], From 0753d99519b0c90f0f9f280b73783b537900dc16 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 11 Nov 2021 20:28:48 -0500 Subject: [PATCH 034/700] Improve Python 2 only syntax detection (GH-2592) * Improve Python 2 only syntax detection First of all this fixes a mistake I made in Python 2 deprecation PR using token.* to check for print/exec statements. Turns out that for nodes with a type value higher than 256 its numeric type isn't guaranteed to be constant. Using syms.* instead fixes this. Also add support for the following cases: print "hello, world!" exec "print('hello, world!')" def set_position((x, y), value): pass try: pass except Exception, err: pass raise RuntimeError, "I feel like crashing today :p" `wow_these_really_did_exist` 10L * Add octal support, more test cases, and fixup long ints Co-authored-by: Jelle Zijlstra Co-authored-by: Jelle Zijlstra --- CHANGES.md | 7 +++ src/black/__init__.py | 36 +++++++++++-- src/black/mode.py | 12 +++++ src/blib2to3/pgen2/token.py | 3 -- tests/data/python2_detection.py | 90 +++++++++++++++++++++++++++++++++ tests/test_black.py | 15 ++++++ 6 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 tests/data/python2_detection.py diff --git a/CHANGES.md b/CHANGES.md index b454a73edab..215680ff2ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Change Log +## _Unreleased_ + +### _Black_ + +- Warn about Python 2 deprecation in more cases by improving Python 2 only syntax + detection (#2592) + ## 21.10b0 ### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index ba4d3dea70e..ad4ee1a0d1a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1132,8 +1132,17 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 features.add(Feature.F_STRINGS) elif n.type == token.NUMBER: - if "_" in n.value: # type: ignore + assert isinstance(n, Leaf) + if "_" in n.value: features.add(Feature.NUMERIC_UNDERSCORES) + elif n.value.endswith(("L", "l")): + # Python 2: 10L + features.add(Feature.LONG_INT_LITERAL) + elif len(n.value) >= 2 and n.value[0] == "0" and n.value[1].isdigit(): + # Python 2: 0123; 00123; ... + if not all(char == "0" for char in n.value): + # although we don't want to match 0000 or similar + features.add(Feature.OCTAL_INT_LITERAL) elif n.type == token.SLASH: if n.parent and n.parent.type in { @@ -1171,10 +1180,31 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 if argch.type in STARS: features.add(feature) - elif n.type == token.PRINT_STMT: + # Python 2 only features (for its deprecation) except for integers, see above + elif n.type == syms.print_stmt: features.add(Feature.PRINT_STMT) - elif n.type == token.EXEC_STMT: + elif n.type == syms.exec_stmt: features.add(Feature.EXEC_STMT) + elif n.type == syms.tfpdef: + # def set_position((x, y), value): + # ... + features.add(Feature.AUTOMATIC_PARAMETER_UNPACKING) + elif n.type == syms.except_clause: + # try: + # ... + # except Exception, err: + # ... + if len(n.children) >= 4: + if n.children[-2].type == token.COMMA: + features.add(Feature.COMMA_STYLE_EXCEPT) + elif n.type == syms.raise_stmt: + # raise Exception, "msg" + if len(n.children) >= 4: + if n.children[-2].type == token.COMMA: + features.add(Feature.COMMA_STYLE_RAISE) + elif n.type == token.BACKQUOTE: + # `i'm surprised this ever existed` + features.add(Feature.BACKQUOTE_REPR) return features diff --git a/src/black/mode.py b/src/black/mode.py index 374c47a42eb..01ee336366c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -44,6 +44,12 @@ class Feature(Enum): # temporary for Python 2 deprecation PRINT_STMT = 200 EXEC_STMT = 201 + AUTOMATIC_PARAMETER_UNPACKING = 202 + COMMA_STYLE_EXCEPT = 203 + COMMA_STYLE_RAISE = 204 + LONG_INT_LITERAL = 205 + OCTAL_INT_LITERAL = 206 + BACKQUOTE_REPR = 207 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { @@ -51,6 +57,12 @@ class Feature(Enum): Feature.ASYNC_IDENTIFIERS, Feature.PRINT_STMT, Feature.EXEC_STMT, + Feature.AUTOMATIC_PARAMETER_UNPACKING, + Feature.COMMA_STYLE_EXCEPT, + Feature.COMMA_STYLE_RAISE, + Feature.LONG_INT_LITERAL, + Feature.OCTAL_INT_LITERAL, + Feature.BACKQUOTE_REPR, }, TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index 349ba8023a2..1e0dec9c714 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -74,9 +74,6 @@ COLONEQUAL: Final = 59 N_TOKENS: Final = 60 NT_OFFSET: Final = 256 -# temporary for Python 2 deprecation -PRINT_STMT: Final = 316 -EXEC_STMT: Final = 288 # --end constants-- tok_name: Final[Dict[int, str]] = {} diff --git a/tests/data/python2_detection.py b/tests/data/python2_detection.py new file mode 100644 index 00000000000..8de2bb58adc --- /dev/null +++ b/tests/data/python2_detection.py @@ -0,0 +1,90 @@ +# This uses a similar construction to the decorators.py test data file FYI. + +print "hello, world!" + +### + +exec "print('hello, world!')" + +### + +def set_position((x, y), value): + pass + +### + +try: + pass +except Exception, err: + pass + +### + +raise RuntimeError, "I feel like crashing today :p" + +### + +`wow_these_really_did_exist` + +### + +10L + +### + +10l + +### + +0123 + +# output + +print("hello python three!") + +### + +exec("I'm not sure if you can use exec like this but that's not important here!") + +### + +try: + pass +except make_exception(1, 2): + pass + +### + +try: + pass +except Exception as err: + pass + +### + +raise RuntimeError(make_msg(1, 2)) + +### + +raise RuntimeError("boom!",) + +### + +def set_position(x, y, value): + pass + +### + +10 + +### + +0 + +### + +000 + +### + +0o12 \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index b96a5438557..7dbc3809d26 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2017,6 +2017,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: ) +@pytest.mark.python2 @pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"]) def test_python_2_deprecation_with_target_version(explicit: bool) -> None: args = [ @@ -2032,6 +2033,20 @@ def test_python_2_deprecation_with_target_version(explicit: bool) -> None: assert "DEPRECATION: Python 2 support will be removed" in result.stderr +@pytest.mark.python2 +def test_python_2_deprecation_autodetection_extended() -> None: + # this test has a similar construction to test_get_features_used_decorator + python2, non_python2 = read_data("python2_detection") + for python2_case in python2.split("###"): + node = black.lib2to3_parse(python2_case) + assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case + for non_python2_case in non_python2.split("###"): + node = black.lib2to3_parse(non_python2_case) + assert black.detect_target_versions(node) != { + TargetVersion.PY27 + }, non_python2_case + + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 53cabe7265eba322e102572acf8af69ea9b2fd62 Mon Sep 17 00:00:00 2001 From: Kian Meng Ang Date: Fri, 12 Nov 2021 11:02:43 +0800 Subject: [PATCH 035/700] Fix typos (#2603) --- CHANGES.md | 2 +- docs/the_black_code_style/index.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 215680ff2ee..4b8dc57388c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,7 +14,7 @@ - Document stability policy, that will apply for non-beta releases (#2529) - Add new `--workers` parameter (#2514) - Fixed feature detection for positional-only arguments in lambdas (#2532) -- Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519) +- Bumped typed-ast version minimum to 1.4.3 for 3.10 compatibility (#2519) - Fixed a Python 3.10 compatibility issue where the loop argument was still being passed even though it has been removed (#2580) - Deprecate Python 2 formatting support (#2523) diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst index 7c2e1753937..d53703277e4 100644 --- a/docs/the_black_code_style/index.rst +++ b/docs/the_black_code_style/index.rst @@ -10,8 +10,8 @@ The Black Code Style *Black* is a PEP 8 compliant opinionated formatter with its own 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 accomodate for new features -in the Python language and, ocassionally, in response to user feedback. +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. Stability Policy ---------------- @@ -32,7 +32,7 @@ versions of *Black*: improved formatting enabled by newer Python language syntax as well as due to improvements in the formatting logic. -- The ``--future`` flag is exempt from this policy. There are no guarentees +- The ``--future`` flag is exempt from this policy. There are no guarantees around the stability of the output with that flag passed into *Black*. This flag is intended for allowing experimentation with the proposed changes to the *Black* code style. From 5e191c29d4492d14f9090a924a77b3dc055443a6 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 12 Nov 2021 20:41:46 -0500 Subject: [PATCH 036/700] Bump deps in Pipfile.lock (GH-2605) Mostly because the hashes for typed-ast were valid for 1.4.2 when the version is pinned to 1.4.3 ... pipenv is pleasant to use /s --- Pipfile.lock | 1891 +++++++++++++++++++++++++++++++------------------- 1 file changed, 1165 insertions(+), 726 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 2ddeca88fff..9d0f708bead 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,54 +16,105 @@ "default": { "aiohttp": { "hashes": [ - "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", - "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", - "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", - "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", - "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", - "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", - "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", - "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", - "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", - "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", - "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", - "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", - "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", - "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", - "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", - "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", - "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", - "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", - "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", - "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", - "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", - "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", - "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", - "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", - "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", - "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", - "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", - "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", - "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", - "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", - "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", - "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", - "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", - "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", - "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", - "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", - "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" + "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984", + "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3", + "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8", + "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4", + "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33", + "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b", + "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e", + "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc", + "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7", + "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c", + "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8", + "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934", + "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079", + "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919", + "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f", + "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795", + "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6", + "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c", + "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e", + "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769", + "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b", + "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671", + "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0", + "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c", + "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4", + "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94", + "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e", + "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0", + "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3", + "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2", + "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41", + "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990", + "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847", + "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a", + "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993", + "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385", + "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3", + "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a", + "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19", + "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739", + "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4", + "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4", + "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9", + "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769", + "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4", + "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3", + "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab", + "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5", + "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe", + "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8", + "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9", + "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e", + "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd", + "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5", + "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45", + "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195", + "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc", + "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150", + "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e", + "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d", + "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661", + "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3", + "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e", + "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c", + "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f", + "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321", + "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f", + "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d", + "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba", + "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19", + "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722", + "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79" ], "index": "pypi", - "version": "==3.7.4.post0" + "version": "==3.8.0" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "async-timeout": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690", + "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.0.1" + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "asynctest": { + "hashes": [ + "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676", + "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac" + ], + "markers": "python_version < '3.8'", + "version": "==0.13.0" }, "attrs": { "hashes": [ @@ -80,21 +131,21 @@ ], "path": "." }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.5'", + "version": "==2.0.7" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "index": "pypi", - "version": "==8.0.1" + "version": "==8.0.3" }, "dataclasses": { "hashes": [ @@ -105,13 +156,91 @@ "markers": "python_version < '3.7'", "version": "==0.8" }, + "frozenlist": { + "hashes": [ + "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c", + "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9", + "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00", + "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161", + "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193", + "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c", + "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d", + "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315", + "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f", + "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9", + "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4", + "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a", + "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020", + "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b", + "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d", + "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a", + "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f", + "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4", + "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837", + "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3", + "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257", + "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38", + "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03", + "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc", + "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43", + "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d", + "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b", + "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f", + "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210", + "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3", + "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de", + "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6", + "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a", + "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b", + "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee", + "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19", + "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15", + "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3", + "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59", + "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b", + "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73", + "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca", + "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4", + "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae", + "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034", + "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9", + "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2", + "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676", + "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618", + "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4", + "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc", + "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397", + "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a", + "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd", + "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729", + "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408", + "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d", + "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0", + "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53", + "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f", + "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2", + "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b", + "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d", + "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792", + "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697", + "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2", + "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb", + "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d", + "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0", + "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367", + "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673", + "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" + }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3.5'", - "version": "==3.2" + "version": "==3.3" }, "idna-ssl": { "hashes": [ @@ -122,54 +251,89 @@ }, "importlib-metadata": { "hashes": [ - "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f", - "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5" + "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", + "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" ], "markers": "python_version < '3.8'", - "version": "==4.6.4" + "version": "==4.8.2" }, "multidict": { "hashes": [ - "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", - "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", - "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", - "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", - "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", - "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", - "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", - "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", - "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", - "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", - "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", - "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", - "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", - "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", - "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", - "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", - "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", - "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", - "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", - "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", - "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", - "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", - "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", - "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", - "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", - "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", - "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", - "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", - "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", - "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", - "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", - "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", - "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", - "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", - "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", - "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", - "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" + "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", + "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", + "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", + "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", + "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", + "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", + "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", + "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", + "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", + "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", + "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", + "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", + "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", + "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", + "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", + "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", + "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", + "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", + "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", + "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", + "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", + "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", + "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", + "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", + "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", + "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", + "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", + "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", + "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", + "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", + "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", + "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", + "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", + "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", + "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", + "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", + "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", + "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", + "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", + "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", + "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", + "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", + "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", + "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", + "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", + "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", + "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", + "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", + "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", + "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", + "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", + "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", + "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", + "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", + "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", + "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", + "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", + "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", + "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", + "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", + "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", + "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", + "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", + "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", + "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", + "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", + "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", + "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", + "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", + "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", + "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", + "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" ], "markers": "python_version >= '3.6'", - "version": "==5.1.0" + "version": "==5.2.0" }, "mypy-extensions": { "hashes": [ @@ -181,11 +345,11 @@ }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.2" }, "pathspec": { "hashes": [ @@ -197,227 +361,321 @@ }, "platformdirs": { "hashes": [ - "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", - "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.4.0" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.4.7" }, "regex": { "hashes": [ - "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd", - "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642", - "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1", - "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321", - "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529", - "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36", - "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a", - "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30", - "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce", - "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376", - "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd", - "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586", - "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7", - "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9", - "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea", - "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94", - "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3", - "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f", - "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267", - "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc", - "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23", - "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882", - "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc", - "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe", - "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759", - "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456", - "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239", - "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb", - "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948", - "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0", - "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183", - "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92", - "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade", - "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044", - "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee", - "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033", - "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2", - "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5", - "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2", - "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504", - "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a" + "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", + "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", + "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", + "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", + "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", + "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", + "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", + "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", + "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", + "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", + "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", + "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", + "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", + "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", + "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", + "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", + "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", + "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", + "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", + "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", + "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", + "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", + "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", + "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", + "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", + "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", + "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", + "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", + "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", + "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", + "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", + "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", + "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", + "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", + "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", + "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", + "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", + "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", + "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", + "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", + "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", + "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", + "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", + "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", + "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", + "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", + "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", + "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", + "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" ], "index": "pypi", - "version": "==2021.8.21" + "version": "==2021.11.10" + }, + "setuptools": { + "hashes": [ + "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf", + "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729" + ], + "markers": "python_version >= '3.6'", + "version": "==58.5.3" }, "setuptools-scm": { "extras": [ "toml" ], "hashes": [ - "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", - "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" + "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119", + "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2" ], "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "version": "==6.3.2" }, "tomli": { "hashes": [ - "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", - "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" ], "index": "pypi", - "version": "==1.2.1" + "version": "==1.2.2" }, "typed-ast": { "hashes": [ - "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", - "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", - "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", - "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", - "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", - "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", - "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", - "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", - "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", - "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", - "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", - "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", - "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", - "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", - "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", - "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", - "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", - "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", - "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", - "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", - "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", - "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", - "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", - "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", - "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", - "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", - "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", - "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", - "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", - "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", + "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", + "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", + "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", + "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", + "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", + "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", + "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", + "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", + "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", + "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", + "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", + "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", + "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", + "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", + "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", + "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", + "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", + "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", + "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", + "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", + "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", + "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", + "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", + "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", + "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", + "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", + "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", + "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", + "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" ], "index": "pypi", "version": "==1.4.3" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", "markers": "python_version < '3.10'", - "version": "==3.10.0.0" + "version": "==3.10.0.2" }, "yarl": { "hashes": [ - "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", - "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", - "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", - "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", - "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", - "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", - "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", - "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", - "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", - "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", - "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", - "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", - "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", - "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", - "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", - "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", - "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", - "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", - "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", - "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", - "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", - "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", - "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", - "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", - "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", - "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", - "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", - "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", - "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", - "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", - "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", - "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", - "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", - "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", - "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", - "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", - "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" + "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", + "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", + "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", + "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", + "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", + "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", + "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", + "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", + "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", + "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", + "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", + "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", + "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", + "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", + "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", + "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", + "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", + "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", + "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", + "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", + "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", + "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", + "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", + "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", + "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", + "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", + "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", + "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", + "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", + "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", + "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", + "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", + "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", + "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", + "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", + "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", + "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", + "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", + "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", + "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", + "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", + "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", + "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", + "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", + "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", + "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", + "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", + "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", + "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", + "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", + "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", + "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", + "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", + "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", + "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", + "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", + "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", + "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", + "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", + "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", + "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", + "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", + "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", + "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", + "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", + "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", + "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", + "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", + "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", + "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", + "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", + "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" ], "markers": "python_version >= '3.6'", - "version": "==1.6.3" + "version": "==1.7.2" }, "zipp": { "hashes": [ - "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", - "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "version": "==3.6.0" } }, "develop": { "aiohttp": { "hashes": [ - "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", - "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", - "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", - "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", - "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", - "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", - "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", - "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", - "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", - "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", - "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", - "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", - "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", - "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", - "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", - "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", - "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", - "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", - "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", - "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", - "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", - "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", - "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", - "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", - "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", - "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", - "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", - "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", - "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", - "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", - "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", - "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", - "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", - "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", - "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", - "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", - "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" + "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984", + "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3", + "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8", + "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4", + "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33", + "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b", + "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e", + "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc", + "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7", + "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c", + "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8", + "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934", + "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079", + "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919", + "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f", + "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795", + "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6", + "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c", + "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e", + "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769", + "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b", + "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671", + "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0", + "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c", + "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4", + "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94", + "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e", + "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0", + "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3", + "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2", + "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41", + "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990", + "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847", + "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a", + "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993", + "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385", + "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3", + "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a", + "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19", + "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739", + "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4", + "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4", + "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9", + "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769", + "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4", + "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3", + "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab", + "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5", + "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe", + "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8", + "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9", + "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e", + "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd", + "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5", + "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45", + "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195", + "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc", + "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150", + "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e", + "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d", + "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661", + "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3", + "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e", + "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c", + "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f", + "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321", + "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f", + "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d", + "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba", + "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19", + "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722", + "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79" ], "index": "pypi", - "version": "==3.7.4.post0" + "version": "==3.8.0" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "alabaster": { "hashes": [ @@ -428,11 +686,19 @@ }, "async-timeout": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690", + "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.0.1" + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "asynctest": { + "hashes": [ + "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676", + "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac" + ], + "markers": "python_version < '3.8'", + "version": "==0.13.0" }, "attrs": { "hashes": [ @@ -459,11 +725,11 @@ }, "backports.entry-points-selectable": { "hashes": [ - "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a", - "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc" + "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b", + "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386" ], "markers": "python_version >= '2.7'", - "version": "==1.1.0" + "version": "==1.1.1" }, "black": { "editable": true, @@ -474,100 +740,97 @@ }, "bleach": { "hashes": [ - "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d", - "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8" + "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", + "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" ], "markers": "python_version >= '3.6'", - "version": "==4.0.0" + "version": "==4.1.0" }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, "cffi": { "hashes": [ - "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", - "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", - "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", - "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", - "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", - "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", - "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", - "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", - "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", - "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", - "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", - "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", - "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", - "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", - "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", - "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", - "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", - "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", - "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", - "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", - "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", - "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", - "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", - "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", - "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", - "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", - "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", - "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", - "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", - "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", - "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", - "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", - "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", - "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", - "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", - "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", - "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", - "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", - "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", - "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", - "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", - "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", - "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", - "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", - "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" - ], - "version": "==1.14.6" + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" }, "cfgv": { "hashes": [ - "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1", - "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1" + "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", + "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" ], "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.0" - }, - "chardet": { - "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "version": "==3.3.1" }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "markers": "python_version >= '3'", - "version": "==2.0.4" + "markers": "python_version >= '3.5'", + "version": "==2.0.7" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "index": "pypi", - "version": "==8.0.1" + "version": "==8.0.3" }, "colorama": { "hashes": [ @@ -579,81 +842,82 @@ }, "coverage": { "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954", + "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0", + "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193", + "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052", + "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e", + "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c", + "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d", + "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4", + "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186", + "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d", + "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696", + "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13", + "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2", + "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c", + "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388", + "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9", + "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59", + "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225", + "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e", + "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b", + "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a", + "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f", + "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93", + "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758", + "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b", + "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b", + "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204", + "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71", + "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd", + "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373", + "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4", + "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc", + "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266", + "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263", + "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf", + "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c", + "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c", + "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc", + "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c", + "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649", + "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972", + "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f", + "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929", + "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d", + "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de", + "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091", + "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab" ], "index": "pypi", - "version": "==5.5" + "version": "==6.1.2" }, "cryptography": { "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586", - "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6", + "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6", + "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c", + "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999", + "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e", + "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992", + "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d", + "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588", + "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa", + "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d", + "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd", + "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d", + "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953", + "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2", + "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8", + "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6", + "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9", + "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6", + "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad", + "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76" ], "markers": "python_version >= '3.6'", - "version": "==3.4.7" + "version": "==35.0.0" }, "dataclasses": { "hashes": [ @@ -666,18 +930,18 @@ }, "decorator": { "hashes": [ - "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323", - "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5" + "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374", + "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" ], "markers": "python_version >= '3.5'", - "version": "==5.0.9" + "version": "==5.1.0" }, "distlib": { "hashes": [ - "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736", - "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c" + "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31", + "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05" ], - "version": "==0.3.2" + "version": "==0.3.3" }, "docutils": { "hashes": [ @@ -697,42 +961,121 @@ }, "filelock": { "hashes": [ - "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", - "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8", + "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b" ], - "version": "==3.0.12" + "markers": "python_version >= '3.6'", + "version": "==3.3.2" }, "flake8": { "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" ], "index": "pypi", - "version": "==3.9.2" + "version": "==4.0.1" }, "flake8-bugbear": { "hashes": [ - "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7", - "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa" + "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769", + "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2" ], "index": "pypi", - "version": "==21.4.3" + "version": "==21.9.2" + }, + "frozenlist": { + "hashes": [ + "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c", + "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9", + "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00", + "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161", + "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193", + "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c", + "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d", + "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315", + "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f", + "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9", + "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4", + "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a", + "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020", + "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b", + "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d", + "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a", + "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f", + "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4", + "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837", + "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3", + "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257", + "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38", + "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03", + "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc", + "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43", + "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d", + "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b", + "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f", + "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210", + "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3", + "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de", + "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6", + "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a", + "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b", + "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee", + "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19", + "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15", + "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3", + "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59", + "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b", + "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73", + "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca", + "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4", + "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae", + "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034", + "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9", + "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2", + "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676", + "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618", + "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4", + "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc", + "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397", + "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a", + "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd", + "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729", + "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408", + "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d", + "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0", + "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53", + "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f", + "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2", + "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b", + "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d", + "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792", + "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697", + "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2", + "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb", + "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d", + "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0", + "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367", + "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673", + "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "identify": { "hashes": [ - "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c", - "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79" + "sha256:6f0368ba0f21c199645a331beb7425d5374376e71bc149e9cb55e45cb45f832d", + "sha256:ba945bddb4322394afcf3f703fa68eda08a6acc0f99d9573eb2be940aa7b9bba" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.13" + "version": "==2.3.5" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3.5'", - "version": "==3.2" + "version": "==3.3" }, "idna-ssl": { "hashes": [ @@ -743,27 +1086,27 @@ }, "imagesize": { "hashes": [ - "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", - "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", + "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.0" + "version": "==1.3.0" }, "importlib-metadata": { "hashes": [ - "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f", - "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5" + "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", + "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" ], "markers": "python_version < '3.8'", - "version": "==4.6.4" + "version": "==4.8.2" }, "importlib-resources": { "hashes": [ - "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977", - "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b" + "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45", + "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b" ], "markers": "python_version < '3.7'", - "version": "==5.2.2" + "version": "==5.4.0" }, "iniconfig": { "hashes": [ @@ -805,19 +1148,19 @@ }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", + "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "version": "==3.0.3" }, "keyring": { "hashes": [ - "sha256:b32397fd7e7063f8dd74a26db910c9862fc2109285fa16e3b5208bcb42a3e579", - "sha256:b7e0156667f5dcc73c1f63a518005cd18a4eb23fe77321194fefcc03748b21a4" + "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe", + "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e" ], "markers": "python_version >= '3.6'", - "version": "==23.1.0" + "version": "==23.2.1" }, "markdown-it-py": { "hashes": [ @@ -832,6 +1175,7 @@ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", @@ -839,6 +1183,7 @@ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", @@ -846,27 +1191,36 @@ "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", + "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", + "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", @@ -874,10 +1228,14 @@ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", + "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -887,14 +1245,6 @@ "markers": "python_version >= '3.6'", "version": "==2.0.1" }, - "matplotlib-inline": { - "hashes": [ - "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811", - "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.2" - }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -912,46 +1262,81 @@ }, "multidict": { "hashes": [ - "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", - "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", - "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", - "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", - "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", - "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", - "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", - "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", - "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", - "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", - "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", - "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", - "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", - "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", - "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", - "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", - "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", - "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", - "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", - "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", - "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", - "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", - "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", - "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", - "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", - "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", - "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", - "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", - "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", - "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", - "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", - "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", - "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", - "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", - "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", - "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", - "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" + "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", + "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", + "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", + "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", + "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", + "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", + "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", + "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", + "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", + "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", + "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", + "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", + "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", + "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", + "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", + "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", + "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", + "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", + "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", + "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", + "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", + "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", + "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", + "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", + "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", + "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", + "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", + "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", + "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", + "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", + "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", + "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", + "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", + "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", + "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", + "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", + "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", + "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", + "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", + "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", + "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", + "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", + "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", + "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", + "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", + "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", + "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", + "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", + "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", + "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", + "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", + "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", + "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", + "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", + "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", + "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", + "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", + "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", + "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", + "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", + "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", + "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", + "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", + "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", + "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", + "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", + "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", + "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", + "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", + "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", + "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", + "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" ], "markers": "python_version >= '3.6'", - "version": "==5.1.0" + "version": "==5.2.0" }, "mypy": { "hashes": [ @@ -992,11 +1377,11 @@ }, "myst-parser": { "hashes": [ - "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603", - "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade" + "sha256:40124b6f27a4c42ac7f06b385e23a9dcd03d84801e9c7130b59b3729a554b1f9", + "sha256:f7f3b2d62db7655cde658eb5d62b2ec2a4631308137bd8d10f296a40d57bbbeb" ], "index": "pypi", - "version": "==0.15.1" + "version": "==0.15.2" }, "nodeenv": { "hashes": [ @@ -1007,11 +1392,11 @@ }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.2" }, "parso": { "hashes": [ @@ -1053,35 +1438,35 @@ }, "platformdirs": { "hashes": [ - "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", - "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.4.0" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "pre-commit": { "hashes": [ - "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c", - "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4" + "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7", + "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6" ], "index": "pypi", - "version": "==2.14.0" + "version": "==2.15.0" }, "prompt-toolkit": { "hashes": [ - "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c", - "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c" + "sha256:449f333dd120bd01f5d296a8ce1452114ba3a71fae7288d2f0ae2c918764fa72", + "sha256:48d85cdca8b6c4f16480c7ce03fd193666b62b0a21667ca56b4bb5ad679d1170" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.20" + "version": "==3.0.22" }, "ptyprocess": { "hashes": [ @@ -1092,35 +1477,34 @@ }, "py": { "hashes": [ - "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", - "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.10.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.8.0" }, "pycparser": { "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" + "version": "==2.21" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" + "version": "==2.4.0" }, "pygments": { "hashes": [ @@ -1135,24 +1519,24 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], "index": "pypi", - "version": "==6.2.4" + "version": "==6.2.5" }, "pytest-cov": { "hashes": [ - "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", - "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], "index": "pypi", - "version": "==2.12.1" + "version": "==3.0.0" }, "pytest-forked": { "hashes": [ @@ -1164,108 +1548,120 @@ }, "pytest-xdist": { "hashes": [ - "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5", - "sha256:ed3d7da961070fce2a01818b51f6888327fb88df4379edeb6b9d990e789d9c8d" + "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168", + "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" ], - "version": "==2021.1" + "version": "==2021.3" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" + "markers": "python_version >= '3.6'", + "version": "==6.0" }, "readme-renderer": { "hashes": [ - "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", - "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" + "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc", + "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8" ], "index": "pypi", - "version": "==29.0" + "version": "==30.0" }, "regex": { "hashes": [ - "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd", - "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642", - "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1", - "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321", - "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529", - "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36", - "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a", - "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30", - "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce", - "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376", - "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd", - "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586", - "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7", - "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9", - "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea", - "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94", - "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3", - "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f", - "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267", - "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc", - "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23", - "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882", - "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc", - "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe", - "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759", - "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456", - "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239", - "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb", - "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948", - "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0", - "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183", - "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92", - "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade", - "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044", - "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee", - "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033", - "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2", - "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5", - "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2", - "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504", - "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a" + "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", + "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", + "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", + "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", + "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", + "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", + "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", + "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", + "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", + "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", + "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", + "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", + "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", + "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", + "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", + "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", + "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", + "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", + "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", + "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", + "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", + "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", + "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", + "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", + "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", + "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", + "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", + "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", + "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", + "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", + "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", + "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", + "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", + "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", + "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", + "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", + "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", + "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", + "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", + "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", + "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", + "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", + "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", + "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", + "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", + "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", + "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", + "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", + "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" ], "index": "pypi", - "version": "==2021.8.21" + "version": "==2021.11.10" }, "requests": { "hashes": [ @@ -1297,23 +1693,31 @@ "markers": "sys_platform == 'linux'", "version": "==3.3.1" }, + "setuptools": { + "hashes": [ + "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf", + "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729" + ], + "markers": "python_version >= '3.6'", + "version": "==58.5.3" + }, "setuptools-scm": { "extras": [ "toml" ], "hashes": [ - "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", - "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" + "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119", + "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2" ], "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "version": "==6.3.2" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snowballstemmer": { @@ -1325,11 +1729,11 @@ }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:6d051ab6e0d06cba786c4656b0fe67ba259fe058410f49e95bee6e49c4052cbf", + "sha256:7e2b30da5f39170efcd95c6270f07669d623c276521fee27ad6c380f49d2bf5b" ], "index": "pypi", - "version": "==4.1.2" + "version": "==4.3.0" }, "sphinx-copybutton": { "hashes": [ @@ -1397,43 +1801,43 @@ }, "tokenize-rt": { "hashes": [ - "sha256:ab339b5ff829eb5e198590477f9c03c84e762b3e455e74c018956e7e326cbc70", - "sha256:b37251fa28c21e8cce2e42f7769a35fba2dd2ecafb297208f9a9a8add3ca7793" + "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8", + "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94" ], "markers": "python_full_version >= '3.6.1'", - "version": "==4.1.0" + "version": "==4.2.1" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "tomli": { "hashes": [ - "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", - "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" ], "index": "pypi", - "version": "==1.2.1" + "version": "==1.2.2" }, "tox": { "hashes": [ - "sha256:9fbf8e2ab758b2a5e7cb2c72945e4728089934853076f67ef18d7575c8ab6b88", - "sha256:c6c4e77705ada004283610fd6d9ba4f77bc85d235447f875df9f0ba1bc23b634" + "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10", + "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca" ], "index": "pypi", - "version": "==3.24.3" + "version": "==3.24.4" }, "tqdm": { "hashes": [ - "sha256:07856e19a1fe4d2d9621b539d3f072fa88c9c1ef1f3b7dd4d4953383134c3164", - "sha256:35540feeaca9ac40c304e916729e6b78045cbbeccd3e941b2868f09306798ac9" + "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", + "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.1" + "version": "==4.62.3" }, "traitlets": { "hashes": [ @@ -1444,97 +1848,97 @@ }, "twine": { "hashes": [ - "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", - "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936" + "sha256:4caad5ef4722e127b3749052fcbffaaf71719b19d4fd4973b29c469957adeba2", + "sha256:916070f8ecbd1985ebed5dbb02b9bda9a092882a96d7069d542d4fc0bb5c673c" ], "index": "pypi", - "version": "==3.4.2" + "version": "==3.6.0" }, "typed-ast": { "hashes": [ - "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", - "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", - "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", - "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", - "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", - "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", - "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", - "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", - "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", - "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", - "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", - "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", - "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", - "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", - "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", - "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", - "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", - "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", - "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", - "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", - "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", - "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", - "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", - "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", - "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", - "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", - "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", - "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", - "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", - "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", + "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", + "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", + "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", + "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", + "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", + "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", + "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", + "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", + "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", + "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", + "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", + "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", + "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", + "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", + "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", + "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", + "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", + "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", + "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", + "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", + "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", + "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", + "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", + "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", + "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", + "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", + "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", + "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", + "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" ], "index": "pypi", "version": "==1.4.3" }, "types-dataclasses": { "hashes": [ - "sha256:248075d093d8f7c1541ce515594df7ae40233d1340afde11ce7125368c5209b8", - "sha256:fc372bb68b878ac7a68fd04230d923d4a6303a137ecb0b9700b90630bdfcbfc9" + "sha256:6568532fed11f854e4db2eb48063385b323b93ecadd09f10a215d56246c306d7", + "sha256:aa45bb0dacdba09e3195a36ff8337bba45eac03b6f31c4645e87b4a2a47830dd" ], "index": "pypi", - "version": "==0.1.7" + "version": "==0.6.1" }, "types-pyyaml": { "hashes": [ - "sha256:745dcb4b1522423026bcc83abb9925fba747f1e8602d902f71a4058f9e7fb662", - "sha256:96f8d3d96aa1a18a465e8f6a220e02cff2f52632314845a364ecbacb0aea6e30" + "sha256:2e27b0118ca4248a646101c5c318dc02e4ca2866d6bc42e84045dbb851555a76", + "sha256:d5b318269652e809b5c30a5fe666c50159ab80bfd41cd6bafe655bf20b29fcba" ], "index": "pypi", - "version": "==5.4.6" + "version": "==6.0.1" }, "types-typed-ast": { "hashes": [ - "sha256:b7f561796b4d002c7522b0020f58b18f715bd28a31429d424a78e2e2dbbd6785", - "sha256:ffa0471e0ba19c4ea0cba0436d660871b5f5215854ea9ead3cb5b60f525af75a" + "sha256:4a261b6af545af41fd08957993292742959ca5c480ee8d49804dcc68d78773a3", + "sha256:d8ea79cbfbc520be8d9bc8de4872f44b342dbdbc091667e2f21b03bbd7969150" ], "index": "pypi", - "version": "==1.4.4" + "version": "==1.5.0" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", "markers": "python_version < '3.10'", - "version": "==3.10.0.0" + "version": "==3.10.0.2" }, "urllib3": { "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.6" + "version": "==1.26.7" }, "virtualenv": { "hashes": [ - "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0", - "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06" + "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814", + "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.7.2" + "version": "==20.10.0" }, "wcwidth": { "hashes": [ @@ -1560,54 +1964,89 @@ }, "yarl": { "hashes": [ - "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", - "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", - "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", - "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", - "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", - "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", - "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", - "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", - "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", - "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", - "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", - "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", - "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", - "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", - "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", - "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", - "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", - "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", - "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", - "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", - "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", - "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", - "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", - "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", - "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", - "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", - "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", - "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", - "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", - "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", - "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", - "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", - "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", - "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", - "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", - "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", - "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" + "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", + "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", + "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", + "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", + "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", + "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", + "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", + "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", + "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", + "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", + "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", + "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", + "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", + "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", + "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", + "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", + "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", + "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", + "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", + "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", + "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", + "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", + "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", + "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", + "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", + "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", + "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", + "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", + "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", + "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", + "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", + "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", + "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", + "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", + "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", + "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", + "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", + "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", + "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", + "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", + "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", + "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", + "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", + "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", + "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", + "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", + "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", + "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", + "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", + "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", + "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", + "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", + "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", + "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", + "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", + "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", + "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", + "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", + "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", + "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", + "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", + "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", + "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", + "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", + "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", + "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", + "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", + "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", + "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", + "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", + "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", + "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" ], "markers": "python_version >= '3.6'", - "version": "==1.6.3" + "version": "==1.7.2" }, "zipp": { "hashes": [ - "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", - "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "version": "==3.6.0" } } } From 1e0ec543ff3a7de715c8ee3359c8defb2c2c0e0d Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sun, 14 Nov 2021 06:15:31 +0300 Subject: [PATCH 037/700] black/parser: partial support for pattern matching (#2586) Partial implementation for #2242. Only works when explicitly stated -t py310. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 3 + src/black/linegen.py | 6 +- src/black/mode.py | 5 + src/black/parsing.py | 3 + src/blib2to3/Grammar.txt | 41 +++++- src/blib2to3/pgen2/driver.py | 81 ++++++++++- src/blib2to3/pgen2/grammar.py | 2 + src/blib2to3/pgen2/parse.py | 135 +++++++++++++++-- src/blib2to3/pgen2/pgen.py | 11 +- src/blib2to3/pygram.py | 19 ++- tests/data/parenthesized_context_managers.py | 21 +++ tests/data/pattern_matching_complex.py | 144 +++++++++++++++++++ tests/data/pattern_matching_simple.py | 92 ++++++++++++ tests/test_format.py | 12 ++ 14 files changed, 553 insertions(+), 22 deletions(-) create mode 100644 tests/data/parenthesized_context_managers.py create mode 100644 tests/data/pattern_matching_complex.py create mode 100644 tests/data/pattern_matching_simple.py diff --git a/CHANGES.md b/CHANGES.md index 4b8dc57388c..b2e8f7439b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ - Warn about Python 2 deprecation in more cases by improving Python 2 only syntax detection (#2592) +- Add partial support for the match statement. As it's experimental, it's only enabled + when `--target-version py310` is explicitly specified (#2586) +- Add support for parenthesized with (#2586) ## 21.10b0 diff --git a/src/black/linegen.py b/src/black/linegen.py index eb53fa0ac56..8cf32c973bb 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -126,7 +126,7 @@ def visit_stmt( """Visit a statement. This implementation is shared for `if`, `while`, `for`, `try`, `except`, - `def`, `with`, `class`, `assert` and assignments. + `def`, `with`, `class`, `assert`, `match`, `case` and assignments. The relevant Python language `keywords` for a given statement will be NAME leaves within it. This methods puts those on a separate line. @@ -292,6 +292,10 @@ def __post_init__(self) -> None: self.visit_async_funcdef = self.visit_async_stmt self.visit_decorated = self.visit_decorators + # PEP 634 + self.visit_match_stmt = partial(v, keywords={"match"}, parens=Ø) + self.visit_case_block = partial(v, keywords={"case"}, parens=Ø) + def transform_line( line: Line, mode: Mode, features: Collection[Feature] = () diff --git a/src/black/mode.py b/src/black/mode.py index 01ee336366c..b24c9c60ded 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -20,6 +20,7 @@ class TargetVersion(Enum): PY37 = 7 PY38 = 8 PY39 = 9 + PY310 = 10 def is_python2(self) -> bool: return self is TargetVersion.PY27 @@ -39,6 +40,7 @@ class Feature(Enum): ASSIGNMENT_EXPRESSIONS = 8 POS_ONLY_ARGUMENTS = 9 RELAXED_DECORATORS = 10 + PATTERN_MATCHING = 11 FORCE_OPTIONAL_PARENTHESES = 50 # temporary for Python 2 deprecation @@ -108,6 +110,9 @@ class Feature(Enum): Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, }, + TargetVersion.PY310: { + Feature.PATTERN_MATCHING, + }, } diff --git a/src/black/parsing.py b/src/black/parsing.py index 0b8d984cedd..fc540ad021d 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -59,6 +59,9 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: # Python 3-compatible code, so only try Python 3 grammar. 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): # Python 3.7+ diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index ac8a067378d..49680323d8b 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -105,7 +105,7 @@ global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* exec_stmt: 'exec' expr ['in' test [',' test]] assert_stmt: 'assert' test [',' test] -compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt +compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt async_stmt: ASYNC (funcdef | with_stmt | for_stmt) if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite] while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite] @@ -115,9 +115,8 @@ try_stmt: ('try' ':' suite ['else' ':' suite] ['finally' ':' suite] | 'finally' ':' suite)) -with_stmt: 'with' with_item (',' with_item)* ':' suite -with_item: test ['as' expr] -with_var: 'as' expr +with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite + # NB compile.c makes sure that the default except clause is last except_clause: 'except' [test [(',' | 'as') test]] suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT @@ -131,7 +130,15 @@ testlist_safe: old_test [(',' old_test)+ [',']] old_test: or_test | old_lambdef old_lambdef: 'lambda' [varargslist] ':' old_test -namedexpr_test: test [':=' test] +namedexpr_test: asexpr_test [':=' asexpr_test] + +# This is actually not a real rule, though since the parser is very +# limited in terms of the strategy about match/case rules, we are inserting +# a virtual case ( as ) as a valid expression. Unless a better +# approach is thought, the only side effect of this seem to be just allowing +# more stuff to be parser (which would fail on the ast). +asexpr_test: test ['as' test] + test: or_test ['if' or_test 'else' test] | lambdef or_test: and_test ('or' and_test)* and_test: not_test ('and' not_test)* @@ -213,3 +220,27 @@ encoding_decl: NAME yield_expr: 'yield' [yield_arg] yield_arg: 'from' test | testlist_star_expr + + +# 3.10 match statement definition + +# PS: normally the grammar is much much more restricted, but +# at this moment for not trying to bother much with encoding the +# exact same DSL in a LL(1) parser, we will just accept an expression +# and let the ast.parse() step of the safe mode to reject invalid +# grammar. + +# The reason why it is more restricted is that, patterns are some +# sort of a DSL (more advanced than our LHS on assignments, but +# still in a very limited python subset). They are not really +# expressions, but who cares. If we can parse them, that is enough +# to reformat them. + +match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT +subject_expr: namedexpr_test + +# cases +case_block: "case" patterns [guard] ':' suite +guard: 'if' namedexpr_test +patterns: pattern ['as' pattern] +pattern: (expr|star_expr) (',' (expr|star_expr))* [','] diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index af1dc6b8aeb..5edd75b1333 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -28,19 +28,92 @@ List, Optional, Text, + Iterator, Tuple, + TypeVar, + Generic, Union, ) +from dataclasses import dataclass, field # Pgen imports from . import grammar, parse, token, tokenize, pgen from logging import Logger from blib2to3.pytree import _Convert, NL from blib2to3.pgen2.grammar import Grammar +from contextlib import contextmanager Path = Union[str, "os.PathLike[str]"] +@dataclass +class ReleaseRange: + start: int + end: Optional[int] = None + tokens: List[Any] = field(default_factory=list) + + def lock(self) -> None: + total_eaten = len(self.tokens) + self.end = self.start + total_eaten + + +class TokenProxy: + def __init__(self, generator: Any) -> None: + self._tokens = generator + self._counter = 0 + self._release_ranges: List[ReleaseRange] = [] + + @contextmanager + def release(self) -> Iterator["TokenProxy"]: + release_range = ReleaseRange(self._counter) + self._release_ranges.append(release_range) + try: + yield self + finally: + # Lock the last release range to the final position that + # has been eaten. + release_range.lock() + + def eat(self, point: int) -> Any: + eaten_tokens = self._release_ranges[-1].tokens + if point < len(eaten_tokens): + return eaten_tokens[point] + else: + while point >= len(eaten_tokens): + token = next(self._tokens) + eaten_tokens.append(token) + return token + + def __iter__(self) -> "TokenProxy": + return self + + def __next__(self) -> Any: + # If the current position is already compromised (looked up) + # return the eaten token, if not just go further on the given + # token producer. + for release_range in self._release_ranges: + assert release_range.end is not None + + start, end = release_range.start, release_range.end + if start <= self._counter < end: + token = release_range.tokens[self._counter - start] + break + else: + token = next(self._tokens) + self._counter += 1 + return token + + def can_advance(self, to: int) -> bool: + # Try to eat, fail if it can't. The eat operation is cached + # so there wont be any additional cost of eating here + try: + self.eat(to) + except StopIteration: + return False + else: + return True + + class Driver(object): def __init__( self, @@ -57,14 +130,18 @@ def __init__( def parse_tokens(self, tokens: Iterable[Any], debug: bool = False) -> NL: """Parse a series of tokens and return the syntax tree.""" # XXX Move the prefix computation into a wrapper around tokenize. + proxy = TokenProxy(tokens) + p = parse.Parser(self.grammar, self.convert) - p.setup() + p.setup(proxy=proxy) + lineno = 1 column = 0 indent_columns = [] type = value = start = end = line_text = None prefix = "" - for quintuple in tokens: + + for quintuple in proxy: type, value, start, end, line_text = quintuple if start != (lineno, column): assert (lineno, column) <= start, ((lineno, column), start) diff --git a/src/blib2to3/pgen2/grammar.py b/src/blib2to3/pgen2/grammar.py index 2882cdac89b..56851070933 100644 --- a/src/blib2to3/pgen2/grammar.py +++ b/src/blib2to3/pgen2/grammar.py @@ -89,6 +89,7 @@ def __init__(self) -> None: self.dfas: Dict[int, DFAS] = {} self.labels: List[Label] = [(0, "EMPTY")] self.keywords: Dict[str, int] = {} + self.soft_keywords: Dict[str, int] = {} self.tokens: Dict[int, int] = {} self.symbol2label: Dict[str, int] = {} self.start = 256 @@ -136,6 +137,7 @@ def copy(self: _P) -> _P: "number2symbol", "dfas", "keywords", + "soft_keywords", "tokens", "symbol2label", ): diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 47c8f02b4f5..dc405264bad 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -9,22 +9,31 @@ how this parsing engine works. """ +import copy +from contextlib import contextmanager # Local imports -from . import token +from . import grammar, token, tokenize from typing import ( + cast, + Any, Optional, Text, Union, Tuple, Dict, List, + Iterator, Callable, Set, + TYPE_CHECKING, ) from blib2to3.pgen2.grammar import Grammar from blib2to3.pytree import NL, Context, RawNode, Leaf, Node +if TYPE_CHECKING: + from blib2to3.driver import TokenProxy + Results = Dict[Text, NL] Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]] @@ -37,6 +46,61 @@ def lam_sub(grammar: Grammar, node: RawNode) -> NL: return Node(type=node[0], children=node[3], context=node[2]) +class Recorder: + def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> None: + self.parser = parser + self._ilabels = ilabels + self.context = context # not really matter + + self._dead_ilabels: Set[int] = set() + self._start_point = copy.deepcopy(self.parser.stack) + self._points = {ilabel: copy.deepcopy(self._start_point) for ilabel in ilabels} + + @property + def ilabels(self) -> Set[int]: + return self._dead_ilabels.symmetric_difference(self._ilabels) + + @contextmanager + def switch_to(self, ilabel: int) -> Iterator[None]: + self.parser.stack = self._points[ilabel] + try: + yield + except ParseError: + self._dead_ilabels.add(ilabel) + finally: + self.parser.stack = self._start_point + + def add_token( + self, tok_type: int, tok_val: Optional[Text], raw: bool = False + ) -> None: + func: Callable[..., Any] + if raw: + func = self.parser._addtoken + else: + func = self.parser.addtoken + + for ilabel in self.ilabels: + with self.switch_to(ilabel): + args = [tok_type, tok_val, self.context] + if raw: + args.insert(0, ilabel) + func(*args) + + def determine_route( + self, value: Optional[Text] = None, force: bool = False + ) -> Optional[int]: + alive_ilabels = self.ilabels + if len(alive_ilabels) == 0: + *_, most_successful_ilabel = self._dead_ilabels + raise ParseError("bad input", most_successful_ilabel, value, self.context) + + ilabel, *rest = alive_ilabels + if force or not rest: + return ilabel + else: + return None + + class ParseError(Exception): """Exception to signal the parser is stuck.""" @@ -114,7 +178,7 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None: self.grammar = grammar self.convert = convert or lam_sub - def setup(self, start: Optional[int] = None) -> None: + def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: """Prepare for parsing. This *must* be called before starting to parse. @@ -137,11 +201,55 @@ def setup(self, start: Optional[int] = None) -> None: self.stack: List[Tuple[DFAS, int, RawNode]] = [stackentry] self.rootnode: Optional[NL] = None self.used_names: Set[str] = set() + self.proxy = proxy def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool: """Add a token; return True iff this is the end of the program.""" # Map from token to label - ilabel = self.classify(type, value, context) + ilabels = self.classify(type, value, context) + assert len(ilabels) >= 1 + + # If we have only one state to advance, we'll directly + # take it as is. + if len(ilabels) == 1: + [ilabel] = ilabels + return self._addtoken(ilabel, type, value, context) + + # If there are multiple states which we can advance (only + # happen under soft-keywords), then we will try all of them + # in parallel and as soon as one state can reach further than + # the rest, we'll choose that one. This is a pretty hacky + # and hopefully temporary algorithm. + # + # For a more detailed explanation, check out this post: + # https://tree.science/what-the-backtracking.html + + with self.proxy.release() as proxy: + counter, force = 0, False + recorder = Recorder(self, ilabels, context) + recorder.add_token(type, value, raw=True) + + next_token_value = value + while recorder.determine_route(next_token_value) is None: + if not proxy.can_advance(counter): + force = True + break + + next_token_type, next_token_value, *_ = proxy.eat(counter) + if next_token_type == tokenize.OP: + next_token_type = grammar.opmap[cast(str, next_token_value)] + + recorder.add_token(next_token_type, next_token_value) + counter += 1 + + ilabel = cast(int, recorder.determine_route(next_token_value, force=force)) + assert ilabel is not None + + return self._addtoken(ilabel, type, value, context) + + def _addtoken( + self, ilabel: int, type: int, value: Optional[Text], context: Context + ) -> bool: # Loop until the token is shifted; may raise exceptions while True: dfa, state, node = self.stack[-1] @@ -185,20 +293,29 @@ def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool: # No success finding a transition raise ParseError("bad input", type, value, context) - def classify(self, type: int, value: Optional[Text], context: Context) -> int: - """Turn a token into a label. (Internal)""" + def classify(self, type: int, value: Optional[Text], context: Context) -> List[int]: + """Turn a token into a label. (Internal) + + Depending on whether the value is a soft-keyword or not, + this function may return multiple labels to choose from.""" if type == token.NAME: # Keep a listing of all used names assert value is not None self.used_names.add(value) # Check for reserved words - ilabel = self.grammar.keywords.get(value) - if ilabel is not None: - return ilabel + if value in self.grammar.keywords: + return [self.grammar.keywords[value]] + elif value in self.grammar.soft_keywords: + assert type in self.grammar.tokens + return [ + self.grammar.soft_keywords[value], + self.grammar.tokens[type], + ] + ilabel = self.grammar.tokens.get(type) if ilabel is None: raise ParseError("bad token", type, value, context) - return ilabel + return [ilabel] def shift( self, type: int, value: Optional[Text], newstate: int, context: Context diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index 564ebbd1184..631682a77c9 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -115,12 +115,17 @@ def make_label(self, c: PgenGrammar, label: Text) -> int: assert label[0] in ('"', "'"), label value = eval(label) if value[0].isalpha(): + if label[0] == '"': + keywords = c.soft_keywords + else: + keywords = c.keywords + # A keyword - if value in c.keywords: - return c.keywords[value] + if value in keywords: + return keywords[value] else: c.labels.append((token.NAME, value)) - c.keywords[value] = ilabel + keywords[value] = ilabel return ilabel else: # An operator (any non-numeric token) diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index b8362b81473..aa20b8104ae 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -39,12 +39,14 @@ class _python_symbols(Symbols): arglist: int argument: int arith_expr: int + asexpr_test: int assert_stmt: int async_funcdef: int async_stmt: int atom: int augassign: int break_stmt: int + case_block: int classdef: int comp_for: int comp_if: int @@ -74,6 +76,7 @@ class _python_symbols(Symbols): for_stmt: int funcdef: int global_stmt: int + guard: int if_stmt: int import_as_name: int import_as_names: int @@ -82,6 +85,7 @@ class _python_symbols(Symbols): import_stmt: int lambdef: int listmaker: int + match_stmt: int namedexpr_test: int not_test: int old_comp_for: int @@ -92,6 +96,8 @@ class _python_symbols(Symbols): or_test: int parameters: int pass_stmt: int + pattern: int + patterns: int power: int print_stmt: int raise_stmt: int @@ -101,6 +107,7 @@ class _python_symbols(Symbols): single_input: int sliceop: int small_stmt: int + subject_expr: int star_expr: int stmt: int subscript: int @@ -124,9 +131,7 @@ class _python_symbols(Symbols): vfplist: int vname: int while_stmt: int - with_item: int with_stmt: int - with_var: int xor_expr: int yield_arg: int yield_expr: int @@ -149,6 +154,7 @@ class _pattern_symbols(Symbols): python_grammar_no_print_statement_no_exec_statement_async_keywords: Grammar python_grammar_no_exec_statement: Grammar pattern_grammar: Grammar +python_grammar_soft_keywords: Grammar python_symbols: _python_symbols pattern_symbols: _pattern_symbols @@ -159,6 +165,7 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: global python_grammar_no_print_statement global python_grammar_no_print_statement_no_exec_statement global python_grammar_no_print_statement_no_exec_statement_async_keywords + global python_grammar_soft_keywords global python_symbols global pattern_grammar global pattern_symbols @@ -171,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) + soft_keywords = python_grammar.soft_keywords.copy() + python_grammar.soft_keywords.clear() python_symbols = _python_symbols(python_grammar) @@ -191,6 +200,12 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: True ) + # 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 + pattern_grammar = driver.load_packaged_grammar( "blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir ) diff --git a/tests/data/parenthesized_context_managers.py b/tests/data/parenthesized_context_managers.py new file mode 100644 index 00000000000..ccf1f94883e --- /dev/null +++ b/tests/data/parenthesized_context_managers.py @@ -0,0 +1,21 @@ +with (CtxManager() as example): + ... + +with (CtxManager1(), CtxManager2()): + ... + +with (CtxManager1() as example, CtxManager2()): + ... + +with (CtxManager1(), CtxManager2() as example): + ... + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/tests/data/pattern_matching_complex.py b/tests/data/pattern_matching_complex.py new file mode 100644 index 00000000000..97ee194fd39 --- /dev/null +++ b/tests/data/pattern_matching_complex.py @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/tests/data/pattern_matching_simple.py b/tests/data/pattern_matching_simple.py new file mode 100644 index 00000000000..5ed62415a4b --- /dev/null +++ b/tests/data/pattern_matching_simple.py @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") diff --git a/tests/test_format.py b/tests/test_format.py index 649c1572bee..4359deea92b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -70,6 +70,11 @@ "percent_precedence", ] +PY310_CASES = [ + "pattern_matching_simple", + "pattern_matching_complex", + "parenthesized_context_managers", +] SOURCES = [ "src/black/__init__.py", @@ -187,6 +192,13 @@ def test_pep_570() -> None: assert_format(source, expected, minimum_version=(3, 8)) +@pytest.mark.parametrize("filename", PY310_CASES) +def test_python_310(filename: str) -> None: + source, expected = read_data(filename) + mode = black.Mode(target_versions={black.TargetVersion.PY310}) + assert_format(source, expected, mode, minimum_version=(3, 10)) + + def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" source, expected = read_data("docstring_no_string_normalization") From eb9d0396cd51065c975e366f06dfea60221a2d03 Mon Sep 17 00:00:00 2001 From: Oliver Margetts Date: Sun, 14 Nov 2021 03:46:15 +0000 Subject: [PATCH 038/700] Allow install under pypy (#2559) Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/workflows/test.yml | 8 +++++++- CHANGES.md | 1 + docs/faq.md | 5 +++++ setup.py | 2 +- src/black/cache.py | 2 +- src/black/parsing.py | 25 +++++++++++++++++-------- tests/test_black.py | 2 +- tox.ini | 27 ++++++++++++++++++++++++++- 8 files changed, 59 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 296ac34a3fb..7ba2a84d049 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -41,9 +41,15 @@ jobs: python -m pip install --upgrade tox - name: Unit tests + if: "!startsWith(matrix.python-version, 'pypy')" run: | tox -e ci-py -- -v --color=yes + - name: Unit tests pypy + if: "startsWith(matrix.python-version, 'pypy')" + run: | + tox -e ci-pypy3 -- -v --color=yes + - name: Publish coverage to Coveralls # If pushed / is a pull request against main repo AND # we're running on Linux (this action only supports Linux) diff --git a/CHANGES.md b/CHANGES.md index b2e8f7439b7..c8b9c849815 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Warn about Python 2 deprecation in more cases by improving Python 2 only syntax detection (#2592) +- Add experimental PyPy support (#2559) - Add partial support for the match statement. As it's experimental, it's only enabled when `--target-version py310` is explicitly specified (#2586) - Add support for parenthesized with (#2586) diff --git a/docs/faq.md b/docs/faq.md index 77f9df51fd4..72bae6b389d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -92,3 +92,8 @@ influence their behavior. While Black does its best to recognize such comments a them in the right place, this detection is not and cannot be perfect. Therefore, you'll sometimes have to manually move these comments to the right place after you format your codebase with _Black_. + +## Can I run black with PyPy? + +Yes, there is support for PyPy 3.7 and higher. You cannot format Python 2 files under +PyPy, because PyPy's inbuilt ast module does not support this. diff --git a/setup.py b/setup.py index de84dc37bb8..a0c2006ef33 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def get_long_description() -> str: "click>=7.1.2", "platformdirs>=2", "tomli>=0.2.6,<2.0.0", - "typed-ast>=1.4.2; python_version < '3.8'", + "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "regex>=2020.1.8", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", diff --git a/src/black/cache.py b/src/black/cache.py index 3f165de2ed6..bca7279f990 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -35,7 +35,7 @@ def read_cache(mode: Mode) -> Cache: with cache_file.open("rb") as fobj: try: cache: Cache = pickle.load(fobj) - except (pickle.UnpicklingError, ValueError): + except (pickle.UnpicklingError, ValueError, IndexError): return {} return cache diff --git a/src/black/parsing.py b/src/black/parsing.py index fc540ad021d..ee6aae1e7ff 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -2,6 +2,7 @@ Parse Python code and perform AST validation. """ import ast +import platform import sys from typing import Iterable, Iterator, List, Set, Union, Tuple @@ -15,10 +16,13 @@ from black.mode import TargetVersion, Feature, supports_feature from black.nodes import syms +_IS_PYPY = platform.python_implementation() == "PyPy" + try: from typed_ast import ast3, ast27 except ImportError: - if sys.version_info < (3, 8): + # Either our python version is too low, or we're on pypy + if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY): print( "The typed_ast package is required but not installed.\n" "You can upgrade to Python 3.8+ or install typed_ast with\n" @@ -117,7 +121,10 @@ def parse_single_version( if sys.version_info >= (3, 8) and version >= (3,): return ast.parse(src, filename, feature_version=version) elif version >= (3,): - return ast3.parse(src, filename, feature_version=version[1]) + if _IS_PYPY: + return ast3.parse(src, filename) + else: + return ast3.parse(src, filename, feature_version=version[1]) elif version == (2, 7): return ast27.parse(src) raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!") @@ -151,12 +158,14 @@ def stringify_ast( yield f"{' ' * depth}{node.__class__.__name__}(" for field in sorted(node._fields): # noqa: F402 - # TypeIgnore has only one field 'lineno' which breaks this comparison - type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore) - if sys.version_info >= (3, 8): - type_ignore_classes += (ast.TypeIgnore,) - if isinstance(node, type_ignore_classes): - break + # TypeIgnore will not be present using pypy < 3.8, so need for this + if not (_IS_PYPY and sys.version_info < (3, 8)): + # TypeIgnore has only one field 'lineno' which breaks this comparison + type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore) + if sys.version_info >= (3, 8): + type_ignore_classes += (ast.TypeIgnore,) + if isinstance(node, type_ignore_classes): + break try: value = getattr(node, field) diff --git a/tests/test_black.py b/tests/test_black.py index 7dbc3809d26..301a3a5b363 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -944,7 +944,7 @@ def test_broken_symlink(self) -> None: symlink = workspace / "broken_link.py" try: symlink.symlink_to("nonexistent.py") - except OSError as e: + except (OSError, NotImplementedError) as e: self.skipTest(f"Can't create symlinks: {e}") self.invokeBlack([str(workspace.resolve())]) diff --git a/tox.ini b/tox.ini index 57f41acb3d1..683a5439ea9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310},fuzz +envlist = {,ci-}py{36,37,38,39,310,py3},fuzz [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -31,6 +31,31 @@ commands = --cov --cov-append {posargs} coverage report +[testenv:{,ci-}pypy3] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +recreate = True +deps = + -r{toxinidir}/test_requirements.txt +; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 +; this seems to cause tox to wait forever +; remove this when pypy releases the bugfix +commands = + pip install -e .[d] + coverage erase + pytest tests --run-optional no_python2 \ + --run-optional no_jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov {posargs} + pip install -e .[jupyter] + pytest tests --run-optional jupyter \ + -m jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov --cov-append {posargs} + coverage report + [testenv:fuzz] skip_install = True deps = From 147d075a4c702ffd6822100dc1f7a6384e52fa57 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sun, 14 Nov 2021 17:04:31 +0300 Subject: [PATCH 039/700] black/parser: support as-exprs within call args (#2608) --- src/blib2to3/Grammar.txt | 1 + tests/data/pattern_matching_extras.py | 9 +++++++++ tests/test_format.py | 1 + 3 files changed, 11 insertions(+) create mode 100644 tests/data/pattern_matching_extras.py diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 49680323d8b..c2a62543abb 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -186,6 +186,7 @@ arglist: argument (',' argument)* [','] # that precede iterable unpackings are blocked; etc. argument: ( test [comp_for] | test ':=' test | + test 'as' test | test '=' test | '**' test | '*' test ) diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py new file mode 100644 index 00000000000..b17922d608b --- /dev/null +++ b/tests/data/pattern_matching_extras.py @@ -0,0 +1,9 @@ +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) diff --git a/tests/test_format.py b/tests/test_format.py index 4359deea92b..8f8ffb3610e 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -73,6 +73,7 @@ PY310_CASES = [ "pattern_matching_simple", "pattern_matching_complex", + "pattern_matching_extras", "parenthesized_context_managers", ] From 3cb010ec8ec02392dee5073b74e6eff80030c5f0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 14 Nov 2021 16:37:06 +0200 Subject: [PATCH 040/700] Declare support for Python 3.10 (#2562) --- CHANGES.md | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c8b9c849815..0d409d778af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Add partial support for the match statement. As it's experimental, it's only enabled when `--target-version py310` is explicitly specified (#2586) - Add support for parenthesized with (#2586) +- Declare support for Python 3.10 for running Black (#2562) ## 21.10b0 diff --git a/setup.py b/setup.py index a0c2006ef33..1914ba745e7 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ def get_long_description() -> str: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", From 78317a4cfb2cc7958ebd553ff6d7cc1aff0d8296 Mon Sep 17 00:00:00 2001 From: Michal Siska <94260368+515k4@users.noreply.github.com> Date: Mon, 15 Nov 2021 17:51:56 +0100 Subject: [PATCH 041/700] Removed distutils import from autoload/black.vim (#2607) (#2610) --- CHANGES.md | 4 ++++ autoload/black.vim | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0d409d778af..c565fbe50ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,10 @@ - Add support for parenthesized with (#2586) - Declare support for Python 3.10 for running Black (#2562) +### Integrations + +- Fixed vim plugin with Python 3.10 by removing deprecated distutils import (#2610) + ## 21.10b0 ### _Black_ diff --git a/autoload/black.vim b/autoload/black.vim index 9ff5c2341fe..6c3bbfea81d 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -3,8 +3,13 @@ import collections import os import sys import vim -from distutils.util import strtobool +def strtobool(text): + if text.lower() in ['y', 'yes', 't', 'true' 'on', '1']: + return True + if text.lower() in ['n', 'no', 'f', 'false' 'off', '0']: + return False + raise ValueError(f"{text} is not convertable to boolean") class Flag(collections.namedtuple("FlagBase", "name, cast")): @property From d7b091e762121ee38ca313ab25006abf4723d203 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 16 Nov 2021 05:38:40 +0300 Subject: [PATCH 042/700] black/parser: optimize deepcopying nodes (#2611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation of the new backtracking logic depends heavily on deepcopying the current state of the parser before seeing one of the new keywords, which by default is an very expensive operations. On my system, formatting these 3 files takes 1.3 seconds. ``` $ touch tests/data/pattern_matching_*; time python -m black -tpy310 tests/data/pattern_matching_* 19ms All done! ✨ 🍰 ✨ 3 files left unchanged. python -m black -tpy310 tests/data/pattern_matching_* 2,09s user 0,04s system 157% cpu 1,357 total ``` which can be optimized 3X if we integrate the existing copying logic (`clone`) to the deepcopy system; ``` $ touch tests/data/pattern_matching_*; time python -m black -tpy310 tests/data/pattern_matching_* 1ms All done! ✨ 🍰 ✨ 3 files left unchanged. python -m black -tpy310 tests/data/pattern_matching_* 0,66s user 0,02s system 147% cpu 0,464 total ``` This still might have some potential, but that would be way trickier than this initial patch. --- src/blib2to3/pytree.py | 5 ++++- tests/data/pattern_matching_extras.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 7843467e012..001652df09f 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -52,7 +52,7 @@ def type_repr(type_num: int) -> Union[Text, int]: return _type_reprs.setdefault(type_num, type_num) -_P = TypeVar("_P") +_P = TypeVar("_P", bound="Base") NL = Union["Node", "Leaf"] Context = Tuple[Text, Tuple[int, int]] @@ -109,6 +109,9 @@ def _eq(self: _P, other: _P) -> bool: """ raise NotImplementedError + def __deepcopy__(self: _P, memo: Any) -> _P: + return self.clone() + def clone(self: _P) -> _P: """ Return a cloned (deep) copy of self. diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index b17922d608b..614e66aebe6 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -1,3 +1,5 @@ +import match + match something: case [a as b]: print(b) @@ -7,3 +9,21 @@ print(b) case Point(int() as x, int() as y): print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case another: + ... + case func(match, case): + ... From 1d7163957a34e8f071aaf9ac59467b912449fb07 Mon Sep 17 00:00:00 2001 From: pszlazak Date: Tue, 16 Nov 2021 03:47:21 +0100 Subject: [PATCH 043/700] Docker image usage description (#2412) Co-authored-by: Jelle Zijlstra --- .../black_docker_image.md | 46 +++++++++++++++++++ docs/usage_and_configuration/index.rst | 2 + 2 files changed, 48 insertions(+) create mode 100644 docs/usage_and_configuration/black_docker_image.md diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md new file mode 100644 index 00000000000..0a458434871 --- /dev/null +++ b/docs/usage_and_configuration/black_docker_image.md @@ -0,0 +1,46 @@ +# Black Docker image + +Official _Black_ Docker images are available on Docker Hub: +https://hub.docker.com/r/pyfound/black + +_Black_ images with the following tags are available: + +- release numbers, e.g. `21.5b2`, `21.6b0`, `21.7b0` etc.\ + ℹ Recommended for users who want to use a particular version of _Black_. +- `latest_release` - tag created when a new version of _Black_ is released.\ + ℹ Recommended for users who want to use released versions of _Black_. It maps to [the latest release](https://github.com/psf/black/releases/latest) + of _Black_. +- `latest` - tag used for the newest image of _Black_.\ + ℹ Recommended for users who always want to use the latest version of _Black_, even before + it is released. + +There is one more tag used for _Black_ Docker images - `latest_non_release`. It is +created for all unreleased +[commits on the `main` branch](https://github.com/psf/black/commits/main). This tag is +not meant to be used by external users. + +## Usage + +A permanent container doesn't have to be created to use _Black_ as a Docker image. It's +enough to run _Black_ commands for the chosen image denoted as `:tag`. In the below +examples, the `latest_release` tag is used. If `:tag` is omitted, the `latest` tag will +be used. + +More about _Black_ usage can be found in +[Usage and Configuration: The basics](./the_basics.md). + +### Check Black version + +```console +$ docker run --rm pyfound/black:latest_release black --version +``` + +### Check code + +```console +$ docker run --rm --volume $(pwd):/src --workdir /src pyfound/black:latest_release black --check . +``` + +_Remark_: besides [regular _Black_ exit codes](./the_basics.md) returned by `--check` +option, [Docker exit codes](https://docs.docker.com/engine/reference/run/#exit-status) +should also be considered. diff --git a/docs/usage_and_configuration/index.rst b/docs/usage_and_configuration/index.rst index 84a9c0cb99b..f6152eec90c 100644 --- a/docs/usage_and_configuration/index.rst +++ b/docs/usage_and_configuration/index.rst @@ -7,6 +7,7 @@ Usage and Configuration the_basics file_collection_and_discovery black_as_a_server + black_docker_image Sometimes, running *Black* with its defaults and passing filepaths to it just won't cut it. Passing each file using paths will become burdensome, and maybe you would like @@ -22,3 +23,4 @@ This section covers features of *Black* and configuring *Black* in detail: - :doc:`The basics <./the_basics>` - :doc:`File collection and discovery ` - :doc:`Black as a server (blackd) <./black_as_a_server>` +- :doc:`Black Docker image <./black_docker_image>` From 1d7260050d846d2ba2dd5bb22944b032245c7e51 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 15 Nov 2021 19:03:47 -0800 Subject: [PATCH 044/700] vim: Parse skip_magic_trailing_comma from pyproject.toml (#2613) Co-authored-by: Kyle Kovacs --- CHANGES.md | 1 + autoload/black.vim | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c565fbe50ca..4e99c9478f1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ ### Integrations - Fixed vim plugin with Python 3.10 by removing deprecated distutils import (#2610) +- The vim plugin now parses `skip_magic_trailing_comma` from pyproject.toml (#2613) ## 21.10b0 diff --git a/autoload/black.vim b/autoload/black.vim index 6c3bbfea81d..66c5b9c2841 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -29,6 +29,7 @@ FLAGS = [ Flag(name="fast", cast=strtobool), Flag(name="skip_string_normalization", cast=strtobool), Flag(name="quiet", cast=strtobool), + Flag(name="skip_magic_trailing_comma", cast=strtobool), ] @@ -143,6 +144,7 @@ def Black(**kwargs): line_length=configs["line_length"], string_normalization=not configs["skip_string_normalization"], is_pyi=vim.current.buffer.name.endswith('.pyi'), + magic_trailing_comma=not configs["skip_magic_trailing_comma"], **black_kwargs, ) quiet = configs["quiet"] From 117891878e5be4d6b771ae5de299e51b679cea27 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 15 Nov 2021 23:24:16 -0500 Subject: [PATCH 045/700] Implementing mypyc support pt. 2 (#2431) --- mypy.ini | 10 +++- pyproject.toml | 3 ++ setup.py | 47 ++++++++++++++----- src/black/__init__.py | 23 +++++++-- src/black/brackets.py | 2 +- src/black/comments.py | 15 ++++-- src/black/files.py | 4 +- src/black/handle_ipynb_magics.py | 13 +++--- src/black/linegen.py | 25 ++++++---- src/black/mode.py | 3 +- src/black/nodes.py | 37 +++++++++------ src/black/output.py | 3 ++ src/black/parsing.py | 26 +++++++++-- src/black/strings.py | 30 +++++++++--- src/black/trans.py | 59 ++++++++++++++--------- src/black_primer/cli.py | 3 +- src/blib2to3/README | 2 + src/blib2to3/pgen2/driver.py | 23 ++++----- src/blib2to3/pgen2/parse.py | 80 +++++++++++++++----------------- src/blib2to3/pgen2/tokenize.py | 33 +++++++------ src/blib2to3/pytree.py | 15 +++--- tests/test_black.py | 22 +++++++-- 22 files changed, 310 insertions(+), 168 deletions(-) diff --git a/mypy.ini b/mypy.ini index 62c1c7fefaa..cfceaa3ee86 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,6 @@ # free to run mypy on Windows, Linux, or macOS and get consistent # results. python_version=3.6 -platform=linux mypy_path=src @@ -24,6 +23,10 @@ warn_redundant_casts=True warn_unused_ignores=True disallow_any_generics=True +# Unreachable blocks have been an issue when compiling mypyc, let's try +# to avoid 'em in the first place. +warn_unreachable=True + # The following are off by default. Flip them on if you feel # adventurous. disallow_untyped_defs=True @@ -32,6 +35,11 @@ check_untyped_defs=True # No incremental mode cache_dir=/dev/null +[mypy-black] +# The following is because of `patch_click()`. Remove when +# we drop Python 3.6 support. +warn_unused_ignores=False + [mypy-black_primer.*] # Until we're not supporting 3.6 primer needs this disallow_any_generics=False diff --git a/pyproject.toml b/pyproject.toml index 73e19608108..aebbc0da29c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,3 +33,6 @@ optional-tests = [ "no_blackd: run when `d` extra NOT installed", "no_jupyter: run when `jupyter` extra NOT installed", ] +markers = [ + "incompatible_with_mypyc: run when testing mypyc compiled black" +] diff --git a/setup.py b/setup.py index 1914ba745e7..7022b24345c 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ assert sys.version_info >= (3, 6, 2), "black requires Python 3.6.2+" from pathlib import Path # noqa E402 +from typing import List # noqa: E402 CURRENT_DIR = Path(__file__).parent sys.path.insert(0, str(CURRENT_DIR)) # for setuptools.build_meta @@ -18,6 +19,17 @@ def get_long_description() -> str: ) +def find_python_files(base: Path) -> List[Path]: + files = [] + for entry in base.iterdir(): + if entry.is_file() and entry.suffix == ".py": + files.append(entry) + elif entry.is_dir(): + files.extend(find_python_files(entry)) + + return files + + USE_MYPYC = False # To compile with mypyc, a mypyc checkout must be present on the PYTHONPATH if len(sys.argv) > 1 and sys.argv[1] == "--use-mypyc": @@ -27,21 +39,34 @@ def get_long_description() -> str: USE_MYPYC = True if USE_MYPYC: + from mypyc.build import mypycify + + src = CURRENT_DIR / "src" + # TIP: filepaths are normalized to use forward slashes and are relative to ./src/ + # before being checked against. + blocklist = [ + # Not performance sensitive, so save bytes + compilation time: + "blib2to3/__init__.py", + "blib2to3/pgen2/__init__.py", + "black/output.py", + "black/concurrency.py", + "black/files.py", + "black/report.py", + # Breaks the test suite when compiled (and is also useless): + "black/debug.py", + # Compiled modules can't be run directly and that's a problem here: + "black/__main__.py", + ] + discovered = [] + # black-primer and blackd have no good reason to be compiled. + discovered.extend(find_python_files(src / "black")) + discovered.extend(find_python_files(src / "blib2to3")) mypyc_targets = [ - "src/black/__init__.py", - "src/blib2to3/pytree.py", - "src/blib2to3/pygram.py", - "src/blib2to3/pgen2/parse.py", - "src/blib2to3/pgen2/grammar.py", - "src/blib2to3/pgen2/token.py", - "src/blib2to3/pgen2/driver.py", - "src/blib2to3/pgen2/pgen.py", + str(p) for p in discovered if p.relative_to(src).as_posix() not in blocklist ] - from mypyc.build import mypycify - opt_level = os.getenv("MYPYC_OPT_LEVEL", "3") - ext_modules = mypycify(mypyc_targets, opt_level=opt_level) + ext_modules = mypycify(mypyc_targets, opt_level=opt_level, verbose=True) else: ext_modules = [] diff --git a/src/black/__init__.py b/src/black/__init__.py index ad4ee1a0d1a..a5ddec91221 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -30,8 +30,9 @@ Union, ) -from dataclasses import replace import click +from dataclasses import replace +from mypy_extensions import mypyc_attr from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES from black.const import STDIN_PLACEHOLDER @@ -66,6 +67,8 @@ from _black_version import version as __version__ +COMPILED = Path(__file__).suffix in (".pyd", ".so") + # types FileContent = str Encoding = str @@ -177,7 +180,12 @@ def validate_regex( raise click.BadParameter("Not a valid regular expression") from None -@click.command(context_settings=dict(help_option_names=["-h", "--help"])) +@click.command( + context_settings=dict(help_option_names=["-h", "--help"]), + # While Click does set this field automatically using the docstring, mypyc + # (annoyingly) strips 'em so we need to set it here too. + help="The uncompromising code formatter.", +) @click.option("-c", "--code", type=str, help="Format the code passed in as a string.") @click.option( "-l", @@ -346,7 +354,10 @@ def validate_regex( " due to exclusion patterns." ), ) -@click.version_option(version=__version__) +@click.version_option( + version=__version__, + message=f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})", +) @click.argument( "src", nargs=-1, @@ -387,7 +398,7 @@ def main( experimental_string_processing: bool, quiet: bool, verbose: bool, - required_version: str, + required_version: Optional[str], include: Pattern[str], exclude: Optional[Pattern[str]], extend_exclude: Optional[Pattern[str]], @@ -655,6 +666,9 @@ def reformat_one( report.failed(src, str(exc)) +# diff-shades depends on being to monkeypatch this function to operate. I know it's +# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 +@mypyc_attr(patchable=True) def reformat_many( sources: Set[Path], fast: bool, @@ -669,6 +683,7 @@ def reformat_many( worker_count = workers if workers is not None else DEFAULT_WORKERS if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 + assert worker_count is not None worker_count = min(worker_count, 60) try: executor = ProcessPoolExecutor(max_workers=worker_count) diff --git a/src/black/brackets.py b/src/black/brackets.py index bb865a0d5b7..c5ed4bf5b9f 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -49,7 +49,7 @@ DOT_PRIORITY: Final = 1 -class BracketMatchError(KeyError): +class BracketMatchError(Exception): """Raised when an opening bracket is unable to be matched to a closing bracket.""" diff --git a/src/black/comments.py b/src/black/comments.py index c7513c21ef5..a8152d687a3 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,8 +1,14 @@ +import sys from dataclasses import dataclass from functools import lru_cache import regex as re from typing import Iterator, List, Optional, Union +if sys.version_info >= (3, 8): + from typing import Final +else: + from typing_extensions import Final + from blib2to3.pytree import Node, Leaf from blib2to3.pgen2 import token @@ -12,11 +18,10 @@ # types LN = Union[Leaf, Node] - -FMT_OFF = {"# fmt: off", "# fmt:off", "# yapf: disable"} -FMT_SKIP = {"# fmt: skip", "# fmt:skip"} -FMT_PASS = {*FMT_OFF, *FMT_SKIP} -FMT_ON = {"# fmt: on", "# fmt:on", "# yapf: enable"} +FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} +FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} +FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} +FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} @dataclass diff --git a/src/black/files.py b/src/black/files.py index 4d7b47aaa9f..560aa05080d 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -17,6 +17,7 @@ TYPE_CHECKING, ) +from mypy_extensions import mypyc_attr from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import tomli @@ -88,13 +89,14 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: return None +@mypyc_attr(patchable=True) def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: """Parse a pyproject toml file, pulling out relevant parts for Black If parsing fails, will raise a tomli.TOMLDecodeError """ with open(path_config, encoding="utf8") as f: - pyproject_toml = tomli.load(f) # type: ignore # due to deprecated API usage + pyproject_toml = tomli.loads(f.read()) config = pyproject_toml.get("tool", {}).get("black", {}) return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index f10eaed4f3e..2fe6739209d 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -333,7 +333,7 @@ def header(self) -> str: return f"%%{self.name}" -@dataclasses.dataclass +# ast.NodeVisitor + dataclass = breakage under mypyc. class CellMagicFinder(ast.NodeVisitor): """Find cell magics. @@ -352,7 +352,8 @@ class CellMagicFinder(ast.NodeVisitor): and we look for instances of the latter. """ - cell_magic: Optional[CellMagic] = None + def __init__(self, cell_magic: Optional[CellMagic] = None) -> None: + self.cell_magic = cell_magic def visit_Expr(self, node: ast.Expr) -> None: """Find cell magic, extract header and body.""" @@ -372,7 +373,8 @@ class OffsetAndMagic: magic: str -@dataclasses.dataclass +# Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here +# as mypyc will generate broken code. class MagicFinder(ast.NodeVisitor): """Visit cell to look for get_ipython calls. @@ -392,9 +394,8 @@ class MagicFinder(ast.NodeVisitor): types of magics). """ - magics: Dict[int, List[OffsetAndMagic]] = dataclasses.field( - default_factory=lambda: collections.defaultdict(list) - ) + def __init__(self) -> None: + self.magics: Dict[int, List[OffsetAndMagic]] = collections.defaultdict(list) def visit_Assign(self, node: ast.Assign) -> None: """Look for system assign magics. diff --git a/src/black/linegen.py b/src/black/linegen.py index 8cf32c973bb..4cba4164fb3 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -5,8 +5,6 @@ import sys from typing import Collection, Iterator, List, Optional, Set, Union -from dataclasses import dataclass, field - 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 @@ -40,7 +38,8 @@ class CannotSplit(CannotTransform): """A readable split that fits the allotted line length is impossible.""" -@dataclass +# This isn't a dataclass because @dataclass + Generic breaks mypyc. +# See also https://github.com/mypyc/mypyc/issues/827. class LineGenerator(Visitor[Line]): """Generates reformatted Line objects. Empty lines are not emitted. @@ -48,9 +47,11 @@ class LineGenerator(Visitor[Line]): in ways that will no longer stringify to valid Python code on the tree. """ - mode: Mode - remove_u_prefix: bool = False - current_line: Line = field(init=False) + def __init__(self, mode: Mode, remove_u_prefix: bool = False) -> None: + self.mode = mode + self.remove_u_prefix = remove_u_prefix + self.current_line: Line + self.__post_init__() def line(self, indent: int = 0) -> Iterator[Line]: """Generate a line. @@ -339,7 +340,9 @@ def transform_line( transformers = [left_hand_split] else: - def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: + def _rhs( + self: object, line: Line, features: Collection[Feature] + ) -> Iterator[Line]: """Wraps calls to `right_hand_split`. The calls increasingly `omit` right-hand trailers (bracket pairs with @@ -366,6 +369,12 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: line, line_length=mode.line_length, features=features ) + # HACK: nested functions (like _rhs) compiled by mypyc don't retain their + # __name__ attribute which is needed in `run_transformer` further down. + # Unfortunately a nested class breaks mypyc too. So a class must be created + # via type ... https://github.com/mypyc/mypyc/issues/884 + rhs = type("rhs", (), {"__call__": _rhs})() + if mode.experimental_string_processing: if line.inside_brackets: transformers = [ @@ -980,7 +989,7 @@ def run_transformer( result.extend(transform_line(transformed_line, mode=mode, features=features)) if ( - transform.__name__ != "rhs" + transform.__class__.__name__ != "rhs" or not line.bracket_tracker.invisible or any(bracket.value for bracket in line.bracket_tracker.invisible) or line.contains_multiline_strings() diff --git a/src/black/mode.py b/src/black/mode.py index b24c9c60ded..3c167569498 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from enum import Enum +from operator import attrgetter from typing import Dict, Set from black.const import DEFAULT_LINE_LENGTH @@ -134,7 +135,7 @@ def get_cache_key(self) -> str: if self.target_versions: version_str = ",".join( str(version.value) - for version in sorted(self.target_versions, key=lambda v: v.value) + for version in sorted(self.target_versions, key=attrgetter("value")) ) else: version_str = "-" diff --git a/src/black/nodes.py b/src/black/nodes.py index 8f2e15b2cc3..36dd1890511 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -15,10 +15,12 @@ Union, ) -if sys.version_info < (3, 8): - from typing_extensions import Final -else: +if sys.version_info >= (3, 8): from typing import Final +else: + from typing_extensions import Final + +from mypy_extensions import mypyc_attr # lib2to3 fork from blib2to3.pytree import Node, Leaf, type_repr @@ -30,7 +32,7 @@ pygram.initialize(CACHE_DIR) -syms = pygram.python_symbols +syms: Final = pygram.python_symbols # types @@ -128,16 +130,21 @@ "//=", } -IMPLICIT_TUPLE = {syms.testlist, syms.testlist_star_expr, syms.exprlist} -BRACKET = {token.LPAR: token.RPAR, token.LSQB: token.RSQB, token.LBRACE: token.RBRACE} -OPENING_BRACKETS = set(BRACKET.keys()) -CLOSING_BRACKETS = set(BRACKET.values()) -BRACKETS = OPENING_BRACKETS | CLOSING_BRACKETS -ALWAYS_NO_SPACE = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT} +IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist} +BRACKET: Final = { + token.LPAR: token.RPAR, + token.LSQB: token.RSQB, + token.LBRACE: token.RBRACE, +} +OPENING_BRACKETS: Final = set(BRACKET.keys()) +CLOSING_BRACKETS: Final = set(BRACKET.values()) +BRACKETS: Final = OPENING_BRACKETS | CLOSING_BRACKETS +ALWAYS_NO_SPACE: Final = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT} RARROW = 55 +@mypyc_attr(allow_interpreted_subclasses=True) class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" @@ -178,9 +185,9 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 `complex_subscript` signals whether the given leaf is part of a subscription which has non-trivial arguments, like arithmetic expressions or function calls. """ - NO = "" - SPACE = " " - DOUBLESPACE = " " + NO: Final = "" + SPACE: Final = " " + DOUBLESPACE: Final = " " t = leaf.type p = leaf.parent v = leaf.value @@ -441,8 +448,8 @@ def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> b 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 = None - last = None + stop_after: Optional[Leaf] = None + last: Optional[Leaf] = None for leaf in reversed(leaves): if stop_after: if leaf is stop_after: diff --git a/src/black/output.py b/src/black/output.py index fd3dbb37627..c85b253c159 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -11,6 +11,7 @@ from click import echo, style +@mypyc_attr(patchable=True) def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: if message is not None: if "bold" not in styles: @@ -19,6 +20,7 @@ def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: echo(message, nl=nl, err=True) +@mypyc_attr(patchable=True) def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: if message is not None: if "fg" not in styles: @@ -27,6 +29,7 @@ def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: echo(message, nl=nl, err=True) +@mypyc_attr(patchable=True) def out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: _out(message, nl=nl, **styles) diff --git a/src/black/parsing.py b/src/black/parsing.py index ee6aae1e7ff..504e20be002 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -4,11 +4,16 @@ import ast import platform import sys -from typing import Iterable, Iterator, List, Set, Union, Tuple +from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final # lib2to3 fork from blib2to3.pytree import Node, Leaf -from blib2to3 import pygram, pytree +from blib2to3 import pygram from blib2to3.pgen2 import driver from blib2to3.pgen2.grammar import Grammar from blib2to3.pgen2.parse import ParseError @@ -16,6 +21,9 @@ from black.mode import TargetVersion, Feature, supports_feature from black.nodes import syms +ast3: Any +ast27: Any + _IS_PYPY = platform.python_implementation() == "PyPy" try: @@ -86,7 +94,7 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - src_txt += "\n" for grammar in get_grammars(set(target_versions)): - drv = driver.Driver(grammar, pytree.convert) + drv = driver.Driver(grammar) try: result = drv.parse_string(src_txt, True) break @@ -148,6 +156,10 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: raise SyntaxError(first_error) +ast3_AST: Final[Type[ast3.AST]] = ast3.AST +ast27_AST: Final[Type[ast27.AST]] = ast27.AST + + def stringify_ast( node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0 ) -> Iterator[str]: @@ -189,7 +201,13 @@ def stringify_ast( elif isinstance(item, (ast.AST, ast3.AST, ast27.AST)): yield from stringify_ast(item, depth + 2) - elif isinstance(value, (ast.AST, ast3.AST, ast27.AST)): + # Note that we are referencing the typed-ast ASTs via global variables and not + # direct module attribute accesses because that breaks mypyc. It's probably + # something to do with the ast3 / ast27 variables being marked as Any leading + # mypy to think this branch is always taken, leaving the rest of the code + # unanalyzed. Tighting up the types for the typed-ast AST types avoids the + # mypyc crash. + elif isinstance(value, (ast.AST, ast3_AST, ast27_AST)): yield from stringify_ast(value, depth + 2) else: diff --git a/src/black/strings.py b/src/black/strings.py index d7b6c240e80..97debe3b5de 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -4,10 +4,20 @@ import regex as re import sys +from functools import lru_cache from typing import List, Pattern +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final -STRING_PREFIX_CHARS = "furbFURB" # All possible string prefix characters. + +STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters. +STRING_PREFIX_RE: Final = re.compile( + r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL +) +FIRST_NON_WHITESPACE_RE: Final = re.compile(r"\s*\t+\s*(\S)") def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: @@ -37,7 +47,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: for line in s.splitlines(): # Find the index of the first non-whitespace character after a string of # whitespace that includes at least one tab - match = re.match(r"\s*\t+\s*(\S)", line) + match = FIRST_NON_WHITESPACE_RE.match(line) if match: first_non_whitespace_idx = match.start(1) @@ -133,7 +143,7 @@ def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str: If remove_u_prefix is given, also removes any u prefix from the string. """ - match = re.match(r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", s, re.DOTALL) + match = STRING_PREFIX_RE.match(s) assert match is not None, f"failed to match string {s!r}" orig_prefix = match.group(1) new_prefix = orig_prefix.replace("F", "f").replace("B", "b").replace("U", "u") @@ -142,6 +152,14 @@ def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str: return f"{new_prefix}{match.group(2)}" +# Re(gex) does actually cache patterns internally but this still improves +# performance on a long list literal of strings by 5-9% since lru_cache's +# caching overhead is much lower. +@lru_cache(maxsize=64) +def _cached_compile(pattern: str) -> re.Pattern: + return re.compile(pattern) + + def normalize_string_quotes(s: str) -> str: """Prefer double quotes but only if it doesn't cause more escaping. @@ -166,9 +184,9 @@ def normalize_string_quotes(s: str) -> str: return s # There's an internal error prefix = s[:first_quote_pos] - unescaped_new_quote = re.compile(rf"(([^\\]|^)(\\\\)*){new_quote}") - escaped_new_quote = re.compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}") - escaped_orig_quote = re.compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}") + unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}") + escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}") + escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}") body = s[first_quote_pos + len(orig_quote) : -len(orig_quote)] if "r" in prefix.casefold(): if unescaped_new_quote.search(body): diff --git a/src/black/trans.py b/src/black/trans.py index 023dcd3618a..d918ef111a2 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -8,6 +8,7 @@ from typing import ( Any, Callable, + ClassVar, Collection, Dict, Iterable, @@ -20,6 +21,14 @@ TypeVar, Union, ) +import sys + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + +from mypy_extensions import trait from black.rusty import Result, Ok, Err @@ -62,7 +71,6 @@ def TErr(err_msg: str) -> Err[CannotTransform]: return Err(cant_transform) -@dataclass # type: ignore class StringTransformer(ABC): """ An implementation of the Transformer protocol that relies on its @@ -90,9 +98,13 @@ class StringTransformer(ABC): as much as possible. """ - line_length: int - normalize_strings: bool - __name__ = "StringTransformer" + __name__: Final = "StringTransformer" + + # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with + # `abc.ABC`. + def __init__(self, line_length: int, normalize_strings: bool) -> None: + self.line_length = line_length + self.normalize_strings = normalize_strings @abstractmethod def do_match(self, line: Line) -> TMatchResult: @@ -184,6 +196,7 @@ class CustomSplit: break_idx: int +@trait class CustomSplitMapMixin: """ This mixin class is used to map merged strings to a sequence of @@ -191,8 +204,10 @@ class CustomSplitMapMixin: the resultant substrings go over the configured max line length. """ - _Key = Tuple[StringID, str] - _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple) + _Key: ClassVar = Tuple[StringID, str] + _CUSTOM_SPLIT_MAP: ClassVar[Dict[_Key, Tuple[CustomSplit, ...]]] = defaultdict( + tuple + ) @staticmethod def _get_key(string: str) -> "CustomSplitMapMixin._Key": @@ -243,7 +258,7 @@ def has_custom_splits(self, string: str) -> bool: return key in self._CUSTOM_SPLIT_MAP -class StringMerger(CustomSplitMapMixin, StringTransformer): +class StringMerger(StringTransformer, CustomSplitMapMixin): """StringTransformer that merges strings together. Requirements: @@ -739,7 +754,7 @@ class BaseStringSplitter(StringTransformer): * The target string is not a multiline (i.e. triple-quote) string. """ - STRING_OPERATORS = [ + STRING_OPERATORS: Final = [ token.EQEQUAL, token.GREATER, token.GREATEREQUAL, @@ -927,7 +942,7 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: return max_string_length -class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): +class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): """ StringTransformer that splits "atom" strings (i.e. strings which exist on lines by themselves). @@ -965,9 +980,9 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): CustomSplit objects and add them to the custom split map. """ - MIN_SUBSTR_SIZE = 6 + MIN_SUBSTR_SIZE: Final = 6 # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. - RE_FEXPR = r""" + RE_FEXPR: Final = r""" (? List[Leaf]: return string_op_leaves -class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): +class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): """ StringTransformer that splits non-"atom" strings (i.e. strings that do not exist on lines by themselves). @@ -1811,20 +1826,20 @@ class StringParser: ``` """ - DEFAULT_TOKEN = -1 + DEFAULT_TOKEN: Final = 20210605 # String Parser States - START = 1 - DOT = 2 - NAME = 3 - PERCENT = 4 - SINGLE_FMT_ARG = 5 - LPAR = 6 - RPAR = 7 - DONE = 8 + START: Final = 1 + DOT: Final = 2 + NAME: Final = 3 + PERCENT: Final = 4 + SINGLE_FMT_ARG: Final = 5 + LPAR: Final = 6 + RPAR: Final = 7 + DONE: Final = 8 # Lookup Table for Next State - _goto: Dict[Tuple[ParserState, NodeType], ParserState] = { + _goto: Final[Dict[Tuple[ParserState, NodeType], ParserState]] = { # A string trailer may start with '.' OR '%'. (START, token.DOT): DOT, (START, token.PERCENT): PERCENT, diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index 2395d35886a..8524b59a632 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -104,13 +104,12 @@ async def async_main( no_diff, ) return int(ret_val) + finally: if not keep and work_path.exists(): LOG.debug(f"Removing {work_path}") rmtree(work_path, onerror=lib.handle_PermissionError) - return -2 - @click.command(context_settings={"help_option_names": ["-h", "--help"]}) @click.option( diff --git a/src/blib2to3/README b/src/blib2to3/README index ccad28337b6..0d3c607c9c7 100644 --- a/src/blib2to3/README +++ b/src/blib2to3/README @@ -19,3 +19,5 @@ Change Log: https://github.com/python/cpython/commit/cae60187cf7a7b26281d012e1952fafe4e2e97e9 - "bpo-42316: Allow unparenthesized walrus operator in indexes (GH-23317)" https://github.com/python/cpython/commit/b0aba1fcdc3da952698d99aec2334faa79a8b68c +- Tweaks to help mypyc compile faster code (including inlining type information, + "Final-ing", etc.) diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 5edd75b1333..8fe820651da 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -23,6 +23,7 @@ import sys from typing import ( Any, + cast, IO, Iterable, List, @@ -34,14 +35,15 @@ Generic, Union, ) +from contextlib import contextmanager from dataclasses import dataclass, field # Pgen imports from . import grammar, parse, token, tokenize, pgen from logging import Logger -from blib2to3.pytree import _Convert, NL +from blib2to3.pytree import NL from blib2to3.pgen2.grammar import Grammar -from contextlib import contextmanager +from blib2to3.pgen2.tokenize import GoodTokenInfo Path = Union[str, "os.PathLike[str]"] @@ -115,29 +117,23 @@ def can_advance(self, to: int) -> bool: class Driver(object): - def __init__( - self, - grammar: Grammar, - convert: Optional[_Convert] = None, - logger: Optional[Logger] = None, - ) -> None: + def __init__(self, grammar: Grammar, logger: Optional[Logger] = None) -> None: self.grammar = grammar if logger is None: logger = logging.getLogger(__name__) self.logger = logger - self.convert = convert - def parse_tokens(self, tokens: Iterable[Any], debug: bool = False) -> NL: + def parse_tokens(self, tokens: Iterable[GoodTokenInfo], debug: bool = False) -> NL: """Parse a series of tokens and return the syntax tree.""" # XXX Move the prefix computation into a wrapper around tokenize. proxy = TokenProxy(tokens) - p = parse.Parser(self.grammar, self.convert) + p = parse.Parser(self.grammar) p.setup(proxy=proxy) lineno = 1 column = 0 - indent_columns = [] + indent_columns: List[int] = [] type = value = start = end = line_text = None prefix = "" @@ -163,6 +159,7 @@ def parse_tokens(self, tokens: Iterable[Any], debug: bool = False) -> NL: if type == token.OP: type = grammar.opmap[value] if debug: + assert type is not None self.logger.debug( "%s %r (prefix=%r)", token.tok_name[type], value, prefix ) @@ -174,7 +171,7 @@ def parse_tokens(self, tokens: Iterable[Any], debug: bool = False) -> NL: elif type == token.DEDENT: _indent_col = indent_columns.pop() prefix, _prefix = self._partially_consume_prefix(prefix, _indent_col) - if p.addtoken(type, value, (prefix, start)): + if p.addtoken(cast(int, type), value, (prefix, start)): if debug: self.logger.debug("Stop.") break diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index dc405264bad..792e8e66698 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -29,7 +29,7 @@ TYPE_CHECKING, ) from blib2to3.pgen2.grammar import Grammar -from blib2to3.pytree import NL, Context, RawNode, Leaf, Node +from blib2to3.pytree import convert, NL, Context, RawNode, Leaf, Node if TYPE_CHECKING: from blib2to3.driver import TokenProxy @@ -70,9 +70,7 @@ def switch_to(self, ilabel: int) -> Iterator[None]: finally: self.parser.stack = self._start_point - def add_token( - self, tok_type: int, tok_val: Optional[Text], raw: bool = False - ) -> None: + def add_token(self, tok_type: int, tok_val: Text, raw: bool = False) -> None: func: Callable[..., Any] if raw: func = self.parser._addtoken @@ -86,9 +84,7 @@ def add_token( args.insert(0, ilabel) func(*args) - def determine_route( - self, value: Optional[Text] = None, force: bool = False - ) -> Optional[int]: + def determine_route(self, value: Text = None, force: bool = False) -> Optional[int]: alive_ilabels = self.ilabels if len(alive_ilabels) == 0: *_, most_successful_ilabel = self._dead_ilabels @@ -164,6 +160,11 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None: to be converted. The syntax tree is converted from the bottom up. + **post-note: the convert argument is ignored since for Black's + usage, convert will always be blib2to3.pytree.convert. Allowing + this to be dynamic hurts mypyc's ability to use early binding. + These docs are left for historical and informational value. + A concrete syntax tree node is a (type, value, context, nodes) tuple, where type is the node type (a token or symbol number), value is None for symbols and a string for tokens, context is @@ -176,6 +177,7 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None: """ self.grammar = grammar + # See note in docstring above. TL;DR this is ignored. self.convert = convert or lam_sub def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: @@ -203,7 +205,7 @@ def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: self.used_names: Set[str] = set() self.proxy = proxy - def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool: + def addtoken(self, type: int, value: Text, context: Context) -> bool: """Add a token; return True iff this is the end of the program.""" # Map from token to label ilabels = self.classify(type, value, context) @@ -237,7 +239,7 @@ def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool: next_token_type, next_token_value, *_ = proxy.eat(counter) if next_token_type == tokenize.OP: - next_token_type = grammar.opmap[cast(str, next_token_value)] + next_token_type = grammar.opmap[next_token_value] recorder.add_token(next_token_type, next_token_value) counter += 1 @@ -247,9 +249,7 @@ def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool: return self._addtoken(ilabel, type, value, context) - def _addtoken( - self, ilabel: int, type: int, value: Optional[Text], context: Context - ) -> bool: + def _addtoken(self, ilabel: int, type: int, value: Text, context: Context) -> bool: # Loop until the token is shifted; may raise exceptions while True: dfa, state, node = self.stack[-1] @@ -257,10 +257,18 @@ def _addtoken( arcs = states[state] # Look for a state with this label for i, newstate in arcs: - t, v = self.grammar.labels[i] - if ilabel == i: + t = self.grammar.labels[i][0] + if t >= 256: + # See if it's a symbol and if we're in its first set + itsdfa = self.grammar.dfas[t] + itsstates, itsfirst = itsdfa + if ilabel in itsfirst: + # Push a symbol + self.push(t, itsdfa, newstate, context) + break # To continue the outer while loop + + elif ilabel == i: # Look it up in the list of labels - assert t < 256 # Shift a token; we're done with it self.shift(type, value, newstate, context) # Pop while we are in an accept-only state @@ -274,14 +282,7 @@ def _addtoken( states, first = dfa # Done with this token return False - elif t >= 256: - # See if it's a symbol and if we're in its first set - itsdfa = self.grammar.dfas[t] - itsstates, itsfirst = itsdfa - if ilabel in itsfirst: - # Push a symbol - self.push(t, self.grammar.dfas[t], newstate, context) - break # To continue the outer while loop + else: if (0, state) in arcs: # An accepting state, pop it and try something else @@ -293,14 +294,13 @@ def _addtoken( # No success finding a transition raise ParseError("bad input", type, value, context) - def classify(self, type: int, value: Optional[Text], context: Context) -> List[int]: + def classify(self, type: int, value: Text, context: Context) -> List[int]: """Turn a token into a label. (Internal) Depending on whether the value is a soft-keyword or not, this function may return multiple labels to choose from.""" if type == token.NAME: # Keep a listing of all used names - assert value is not None self.used_names.add(value) # Check for reserved words if value in self.grammar.keywords: @@ -317,18 +317,13 @@ def classify(self, type: int, value: Optional[Text], context: Context) -> List[i raise ParseError("bad token", type, value, context) return [ilabel] - def shift( - self, type: int, value: Optional[Text], newstate: int, context: Context - ) -> None: + def shift(self, type: int, value: Text, newstate: int, context: Context) -> None: """Shift a token. (Internal)""" dfa, state, node = self.stack[-1] - assert value is not None - assert context is not None rawnode: RawNode = (type, value, context, None) - newnode = self.convert(self.grammar, rawnode) - if newnode is not None: - assert node[-1] is not None - node[-1].append(newnode) + newnode = convert(self.grammar, rawnode) + assert node[-1] is not None + node[-1].append(newnode) self.stack[-1] = (dfa, newstate, node) def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None: @@ -341,12 +336,11 @@ def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None def pop(self) -> None: """Pop a nonterminal. (Internal)""" popdfa, popstate, popnode = self.stack.pop() - newnode = self.convert(self.grammar, popnode) - if newnode is not None: - if self.stack: - dfa, state, node = self.stack[-1] - assert node[-1] is not None - node[-1].append(newnode) - else: - self.rootnode = newnode - self.rootnode.used_names = self.used_names + newnode = convert(self.grammar, popnode) + if self.stack: + dfa, state, node = self.stack[-1] + assert node[-1] is not None + node[-1].append(newnode) + else: + self.rootnode = newnode + self.rootnode.used_names = self.used_names diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index bad79b2dc2c..283fac2d537 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -27,6 +27,7 @@ function to which the 5 fields described above are passed as 5 arguments, each time a new token is found.""" +import sys from typing import ( Callable, Iterable, @@ -39,6 +40,12 @@ Union, cast, ) + +if sys.version_info >= (3, 8): + from typing import Final +else: + from typing_extensions import Final + from blib2to3.pgen2.token import * from blib2to3.pgen2.grammar import Grammar @@ -139,7 +146,7 @@ def _combinations(*l): PseudoExtras = group(r"\\\r?\n", Comment, Triple) PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name) -pseudoprog = re.compile(PseudoToken, re.UNICODE) +pseudoprog: Final = re.compile(PseudoToken, re.UNICODE) single3prog = re.compile(Single3) double3prog = re.compile(Double3) @@ -149,7 +156,7 @@ def _combinations(*l): | {"u", "U", "ur", "uR", "Ur", "UR"} ) -endprogs = { +endprogs: Final = { "'": re.compile(Single), '"': re.compile(Double), "'''": single3prog, @@ -159,12 +166,12 @@ def _combinations(*l): **{prefix: None for prefix in _strprefixes}, } -triple_quoted = ( +triple_quoted: Final = ( {"'''", '"""'} | {f"{prefix}'''" for prefix in _strprefixes} | {f'{prefix}"""' for prefix in _strprefixes} ) -single_quoted = ( +single_quoted: Final = ( {"'", '"'} | {f"{prefix}'" for prefix in _strprefixes} | {f'{prefix}"' for prefix in _strprefixes} @@ -418,7 +425,7 @@ def generate_tokens( logical line; continuation lines are included. """ lnum = parenlev = continued = 0 - numchars = "0123456789" + numchars: Final = "0123456789" contstr, needcont = "", 0 contline: Optional[str] = None indents = [0] @@ -427,7 +434,7 @@ def generate_tokens( # `await` as keywords. async_keywords = False if grammar is None else grammar.async_keywords # 'stashed' and 'async_*' are used for async/await parsing - stashed = None + stashed: Optional[GoodTokenInfo] = None async_def = False async_def_indent = 0 async_def_nl = False @@ -440,7 +447,7 @@ def generate_tokens( line = readline() except StopIteration: line = "" - lnum = lnum + 1 + lnum += 1 pos, max = 0, len(line) if contstr: # continued string @@ -481,14 +488,14 @@ def generate_tokens( column = 0 while pos < max: # measure leading whitespace if line[pos] == " ": - column = column + 1 + column += 1 elif line[pos] == "\t": column = (column // tabsize + 1) * tabsize elif line[pos] == "\f": column = 0 else: break - pos = pos + 1 + pos += 1 if pos == max: break @@ -507,7 +514,7 @@ def generate_tokens( COMMENT, comment_token, (lnum, pos), - (lnum, pos + len(comment_token)), + (lnum, nl_pos), line, ) yield (NL, line[nl_pos:], (lnum, nl_pos), (lnum, len(line)), line) @@ -652,16 +659,16 @@ def generate_tokens( continued = 1 else: if initial in "([{": - parenlev = parenlev + 1 + parenlev += 1 elif initial in ")]}": - parenlev = parenlev - 1 + parenlev -= 1 if stashed: yield stashed stashed = None yield (OP, token, spos, epos, line) else: yield (ERRORTOKEN, line[pos], (lnum, pos), (lnum, pos + 1), line) - pos = pos + 1 + pos += 1 if stashed: yield stashed diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 001652df09f..bd86270b8e2 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -14,7 +14,6 @@ from typing import ( Any, - Callable, Dict, Iterator, List, @@ -92,8 +91,6 @@ def __eq__(self, other: Any) -> bool: return NotImplemented return self._eq(other) - __hash__ = None # type: Any # For Py3 compatibility. - @property def prefix(self) -> Text: raise NotImplementedError @@ -437,7 +434,7 @@ def __str__(self) -> Text: This reproduces the input source exactly. """ - return self.prefix + str(self.value) + return self._prefix + str(self.value) def _eq(self, other) -> bool: """Compare two nodes for equality.""" @@ -672,8 +669,11 @@ def __init__( newcontent = list(content) for i, item in enumerate(newcontent): assert isinstance(item, BasePattern), (i, item) - if isinstance(item, WildcardPattern): - self.wildcards = True + # I don't even think this code is used anywhere, but it does cause + # unreachable errors from mypy. This function's signature does look + # odd though *shrug*. + if isinstance(item, WildcardPattern): # type: ignore[unreachable] + self.wildcards = True # type: ignore[unreachable] self.type = type self.content = newcontent self.name = name @@ -978,6 +978,3 @@ def generate_matches( r.update(r0) r.update(r1) yield c0 + c1, r - - -_Convert = Callable[[Grammar, RawNode], Any] diff --git a/tests/test_black.py b/tests/test_black.py index 301a3a5b363..3d5d3982817 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -122,7 +122,7 @@ def invokeBlack( runner = BlackRunner() if ignore_config: args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args] - result = runner.invoke(black.main, args) + result = runner.invoke(black.main, args, catch_exceptions=False) assert result.stdout_bytes is not None assert result.stderr_bytes is not None msg = ( @@ -841,6 +841,7 @@ def test_get_future_imports(self) -> None: ) self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node)) + @pytest.mark.incompatible_with_mypyc def test_debug_visitor(self) -> None: source, _ = read_data("debug_visitor.py") expected, _ = read_data("debug_visitor.out") @@ -891,6 +892,7 @@ def test_endmarker(self) -> None: self.assertEqual(len(n.children), 1) self.assertEqual(n.children[0].type, black.token.ENDMARKER) + @pytest.mark.incompatible_with_mypyc @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT") def test_assertFormatEqual(self) -> None: out_lines = [] @@ -1055,6 +1057,7 @@ def test_pipe_force_py36(self) -> None: actual = result.output self.assertFormatEqual(actual, expected) + @pytest.mark.incompatible_with_mypyc def test_reformat_one_with_stdin(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1072,6 +1075,7 @@ def test_reformat_one_with_stdin(self) -> None: fsts.assert_called_once() report.done.assert_called_with(path, black.Changed.YES) + @pytest.mark.incompatible_with_mypyc def test_reformat_one_with_stdin_filename(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1094,6 +1098,7 @@ def test_reformat_one_with_stdin_filename(self) -> None: # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) + @pytest.mark.incompatible_with_mypyc def test_reformat_one_with_stdin_filename_pyi(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1118,6 +1123,7 @@ def test_reformat_one_with_stdin_filename_pyi(self) -> None: # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) + @pytest.mark.incompatible_with_mypyc def test_reformat_one_with_stdin_filename_ipynb(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1142,6 +1148,7 @@ def test_reformat_one_with_stdin_filename_ipynb(self) -> None: # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) + @pytest.mark.incompatible_with_mypyc def test_reformat_one_with_stdin_and_existing_path(self) -> None: with patch( "black.format_stdin_to_stdout", @@ -1296,6 +1303,7 @@ def test_read_pyproject_toml(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + @pytest.mark.incompatible_with_mypyc def test_find_project_root(self) -> None: with TemporaryDirectory() as workspace: root = Path(workspace) @@ -1483,6 +1491,7 @@ def test_code_option_color_diff(self) -> None: assert output == result_diff, "The output did not match the expected value." assert result.exit_code == 0, "The exit code is incorrect." + @pytest.mark.incompatible_with_mypyc def test_code_option_safe(self) -> None: """Test that the code option throws an error when the sanity checks fail.""" # Patch black.assert_equivalent to ensure the sanity checks fail @@ -1507,6 +1516,7 @@ def test_code_option_fast(self) -> None: self.compare_results(result, formatted, 0) + @pytest.mark.incompatible_with_mypyc def test_code_option_config(self) -> None: """ Test that the code option finds the pyproject.toml in the current directory. @@ -1527,6 +1537,7 @@ def test_code_option_config(self) -> None: call_args[0].lower() == str(pyproject_path).lower() ), "Incorrect config loaded." + @pytest.mark.incompatible_with_mypyc def test_code_option_parent_config(self) -> None: """ Test that the code option finds the pyproject.toml in the parent directory. @@ -1894,6 +1905,7 @@ def test_extend_exclude(self) -> None: src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude" ) + @pytest.mark.incompatible_with_mypyc def test_symlink_out_of_root_directory(self) -> None: path = MagicMock() root = THIS_DIR.resolve() @@ -2047,8 +2059,12 @@ def test_python_2_deprecation_autodetection_extended() -> None: }, non_python2_case -with open(black.__file__, "r", encoding="utf-8") as _bf: - black_source_lines = _bf.readlines() +try: + with open(black.__file__, "r", encoding="utf-8") as _bf: + black_source_lines = _bf.readlines() +except UnicodeDecodeError: + if not black.COMPILED: + raise def tracefunc( From 0d1b957d400e2884ad312b4c113ee215effb8256 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 16 Nov 2021 00:07:25 -0500 Subject: [PATCH 046/700] Fix 3.10's supported features (#2614) --- src/black/mode.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/black/mode.py b/src/black/mode.py index 3c167569498..e2417531240 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -112,6 +112,15 @@ class Feature(Enum): Feature.POS_ONLY_ARGUMENTS, }, TargetVersion.PY310: { + Feature.UNICODE_LITERALS, + Feature.F_STRINGS, + Feature.NUMERIC_UNDERSCORES, + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.ASYNC_KEYWORDS, + Feature.ASSIGNMENT_EXPRESSIONS, + Feature.RELAXED_DECORATORS, + Feature.POS_ONLY_ARGUMENTS, Feature.PATTERN_MATCHING, }, } From 7dacdbe6dc0cb1024fcf68dabd6b55a0f7f90cf9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 16 Nov 2021 18:22:32 -0800 Subject: [PATCH 047/700] fix vim plugin (#2615) --- plugin/black.vim | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugin/black.vim b/plugin/black.vim index 90d2047790b..dbe236b5f34 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -50,6 +50,13 @@ if !exists("g:black_skip_string_normalization") let g:black_skip_string_normalization = 0 endif endif +if !exists("g:black_skip_magic_trailing_comma") + if exists("g:black_magic_trailing_comma") + let g:black_skip_magic_trailing_comma = !g:black_magic_trailing_comma + else + let g:black_skip_magic_trailing_comma = 0 + endif +endif if !exists("g:black_quiet") let g:black_quiet = 0 endif @@ -64,6 +71,7 @@ function BlackComplete(ArgLead, CmdLine, CursorPos) \ 'target_version=py37', \ 'target_version=py38', \ 'target_version=py39', +\ 'target_version=py310', \ ] endfunction From d0b04d9f219a9777cddf82c98f8bc19f578b943e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 16 Nov 2021 18:30:19 -0800 Subject: [PATCH 048/700] prepare release 21.11b0 (#2616) --- 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 4e99c9478f1..cd1df15a15a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## _Unreleased_ +## 21.11b0 ### _Black_ diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index cf0ef1dfed9..58e3e695522 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.10b0 + rev: 21.11b0 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 533c213a4da..bffc45fddb1 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.10b0 +black, version 21.11b0 ``` An option to require a specific version to be running is also provided. From ecf8c74481bef13e7a6ca68a953ae470de0d3890 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 17 Nov 2021 22:46:28 -0500 Subject: [PATCH 049/700] Bump regex dependency to 2021.4.4 to fix import of Pattern class (#2621) Fixes #2620 --- CHANGES.md | 6 ++++++ Pipfile | 2 +- Pipfile.lock | 26 +++++++++++++++++++++----- setup.py | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cd1df15a15a..9c3be34255d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Bumped regex version minimum to 2021.4.4 to fix Pattern class usage (#2621) + ## 21.11b0 ### _Black_ diff --git a/Pipfile b/Pipfile index 90e8f62d666..9608f4d4ef7 100644 --- a/Pipfile +++ b/Pipfile @@ -42,7 +42,7 @@ platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" -regex = ">=2020.1.8" +regex = ">=2021.4.4" tomli = ">=0.2.6, <2.0.0" typed-ast = "==1.4.3" typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} diff --git a/Pipfile.lock b/Pipfile.lock index 9d0f708bead..a02ea4a2590 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4baa020356174f89177af103f1966928e7b9c2a69df3a9d4e8070eb83ee19387" + "sha256": "a516705ed9270469fd58d20f1b26a94a6ed052451ef7425d82605b80513a65b3" }, "pipfile-spec": 6, "requires": {}, @@ -372,7 +372,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "regex": { @@ -684,6 +684,14 @@ ], "version": "==0.7.12" }, + "appnope": { + "hashes": [ + "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", + "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.2" + }, "async-timeout": { "hashes": [ "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690", @@ -1245,6 +1253,14 @@ "markers": "python_version >= '3.6'", "version": "==2.0.1" }, + "matplotlib-inline": { + "hashes": [ + "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", + "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" + ], + "markers": "python_version >= '3.5'", + "version": "==0.1.3" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -1519,7 +1535,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { @@ -1717,7 +1733,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snowballstemmer": { @@ -1812,7 +1828,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tomli": { diff --git a/setup.py b/setup.py index 7022b24345c..33b7239a13e 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,7 @@ def find_python_files(base: Path) -> List[Path]: "platformdirs>=2", "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", - "regex>=2020.1.8", + "regex>=2021.4.4", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.10.0.0", From 19f6aa8208154de4560ee1e4a3e638e120dcdba5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 17 Nov 2021 19:51:49 -0800 Subject: [PATCH 050/700] prepare release 2021.11b1 (#2622) --- 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 9c3be34255d..35024de724d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.11b1 ### _Black_ diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 58e3e695522..2149027218d 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.11b0 + rev: 21.11b1 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 bffc45fddb1..3dc26b7013f 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.11b0 +black, version 21.11b1 ``` An option to require a specific version to be running is also provided. From 9a73bb86db59de1e12426fec81dcdb7f3bb9be7b Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 18 Nov 2021 22:20:44 -0500 Subject: [PATCH 051/700] Fix mypyc compat issue w/ AST safety check (GH-2628) I can't wait for when we drop Python 2 support FWIW :) --- src/black/parsing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/parsing.py b/src/black/parsing.py index 504e20be002..32cfa5239f1 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -169,6 +169,7 @@ def stringify_ast( yield f"{' ' * depth}{node.__class__.__name__}(" + type_ignore_classes: Tuple[Type[Any], ...] for field in sorted(node._fields): # noqa: F402 # TypeIgnore will not be present using pypy < 3.8, so need for this if not (_IS_PYPY and sys.version_info < (3, 8)): From 05954c0950637aa1039d0ac86a4a7e832cbffd9f Mon Sep 17 00:00:00 2001 From: "Matthew D. Scholefield" Date: Sat, 20 Nov 2021 11:25:30 -0800 Subject: [PATCH 052/700] Fix process pool fallback on Python 3.10 (GH-2631) In Python 3.10 the exception generated by creating a process pool on a Python build that doesn't support this is now `NotImplementedError` Commit history before merge: * Fix process pool fallback on Python 3.10 * Update CHANGES.md * Update CHANGES.md Co-authored-by: Jelle Zijlstra --- CHANGES.md | 6 ++++++ src/black/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 35024de724d..db519ac6bbc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) + ## 21.11b1 ### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index a5ddec91221..aa7970cfd6c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -687,7 +687,7 @@ def reformat_many( worker_count = min(worker_count, 60) try: executor = ProcessPoolExecutor(max_workers=worker_count) - except (ImportError, OSError): + except (ImportError, NotImplementedError, OSError): # we arrive here if the underlying system does not support multi-processing # like in AWS Lambda or Termux, in which case we gracefully fallback to # a ThreadPoolExecutor with just a single worker (more workers would not do us From 40759445c9e5210fb441679d6c0e42921de89ed6 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 21 Nov 2021 15:02:08 +0000 Subject: [PATCH 053/700] Change `cfg` to `ini` for text highlighting (#2632) --- docs/guides/using_black_with_other_tools.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 09421819ec3..9938d814073 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -97,7 +97,7 @@ does not break older versions so you can keep it if you are running previous ver
.isort.cfg -```cfg +```ini [settings] profile = black ``` @@ -107,7 +107,7 @@ profile = black
setup.cfg -```cfg +```ini [isort] profile = black ``` @@ -181,7 +181,7 @@ extend-ignore = E203
setup.cfg -```cfg +```ini [flake8] max-line-length = 88 extend-ignore = E203 From dfa45cec9e4991823b06fa655e3e444391fadb65 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 25 Nov 2021 04:21:36 +0300 Subject: [PATCH 054/700] grammar: accept open sequences on match subject (GH-2639) * grammar: accept open sequences on match subject * give an example about the fixed match subject --- CHANGES.md | 1 + src/blib2to3/Grammar.txt | 2 +- tests/data/pattern_matching_extras.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index db519ac6bbc..94d1c69259a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Black_ - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) +- Fixed `match` statements with open sequence subjects, like `match a, b:` (#2639) ## 21.11b1 diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index c2a62543abb..de9a6a2283f 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -238,7 +238,7 @@ yield_arg: 'from' test | testlist_star_expr # to reformat them. match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT -subject_expr: namedexpr_test +subject_expr: namedexpr_test (',' namedexpr_test)* [','] # cases case_block: "case" patterns [guard] ':' suite diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index 614e66aebe6..d4bba38ee7c 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -27,3 +27,19 @@ def func(match: case, case: match) -> case: ... case func(match, case): ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass From db2715441a391f218863493aa20027f802ab0c7b Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Nov 2021 17:09:47 -0800 Subject: [PATCH 055/700] README: Add KeepTruckin to the list of orgs (GH-2638) At KT, we used Black to format all Python code in our Mono-repo. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9061c33863..2adf60a783a 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,8 @@ Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, and many more. -The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo, -QuantumBlack, Tesla. +The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Mozilla, Quora, +Duolingo, QuantumBlack, Tesla. Are we missing anyone? Let us know. From 17e42cb94b494f0e5d7c80ee842f578a5a3cefcc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Nov 2021 18:34:19 -0800 Subject: [PATCH 056/700] fix regex (#2643) --- tests/test_black.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 3d5d3982817..4267c6110a9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -31,7 +31,7 @@ import click import pytest -import regex as re +import re from click import unstyle from click.testing import CliRunner from pathspec import PathSpec @@ -70,7 +70,7 @@ R = TypeVar("R") # Match the time output in a diff, but nothing else -DIFF_TIME = re.compile(r"\t[\d-:+\. ]+") +DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+") @contextmanager From e0253080b0d2b61bf2105a2f5afdf5173e33d0e5 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Fri, 26 Nov 2021 16:14:57 +0000 Subject: [PATCH 057/700] Assignment to env var in Jupyter Notebook doesn't round-trip (#2642) closes #2641 --- CHANGES.md | 1 + src/black/handle_ipynb_magics.py | 27 +++++++++++++++++---------- tests/test_ipynb.py | 4 ++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 94d1c69259a..d81c1fd4fc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) - Fixed `match` statements with open sequence subjects, like `match a, b:` (#2639) +- Fixed assignment to environment variables in Jupyter Notebooks (#2642) ## 21.11b1 diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 2fe6739209d..5807dac14d0 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -403,20 +403,28 @@ def visit_Assign(self, node: ast.Assign) -> None: For example, black_version = !black --version + env = %env var - would have been transformed to + would have been (respectively) transformed to black_version = get_ipython().getoutput('black --version') + env = get_ipython().run_line_magic('env', 'var') - and we look for instances of the latter. + and we look for instances of any of the latter. """ - if ( - isinstance(node.value, ast.Call) - and _is_ipython_magic(node.value.func) - and node.value.func.attr == "getoutput" - ): - (arg,) = _get_str_args(node.value.args) - src = f"!{arg}" + if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func): + args = _get_str_args(node.value.args) + if node.value.func.attr == "getoutput": + src = f"!{args[0]}" + elif node.value.func.attr == "run_line_magic": + src = f"%{args[0]}" + if args[1]: + src += f" {args[1]}" + else: + raise AssertionError( + "Unexpected IPython magic {node.value.func.attr!r} found. " + "Please report a bug on https://github.com/psf/black/issues." + ) from None self.magics[node.value.lineno].append( OffsetAndMagic(node.value.col_offset, src) ) @@ -451,7 +459,6 @@ def visit_Expr(self, node: ast.Expr) -> None: else: src = f"%{args[0]}" if args[1]: - assert src is not None src += f" {args[1]}" elif node.value.func.attr == "system": src = f"!{args[0]}" diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index ba460074e9a..5ecdf00b7bc 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -90,6 +90,10 @@ def test_cell_magic_noop() -> None: id="Line magic with argument", ), pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"), + pytest.param( + "env = %env var", "env = %env var", id="Assignment to environment variable" + ), + pytest.param("env = %env", "env = %env", id="Assignment to magic"), ), ) def test_magic(src: str, expected: str) -> None: From 72a84d4099f2930979bd1ca1d9e441140b0a304d Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 27 Nov 2021 02:53:16 +0000 Subject: [PATCH 058/700] add missing f-string (#2650) --- src/black/handle_ipynb_magics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 5807dac14d0..8ae9d2e2e6c 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -422,7 +422,7 @@ def visit_Assign(self, node: ast.Assign) -> None: src += f" {args[1]}" else: raise AssertionError( - "Unexpected IPython magic {node.value.func.attr!r} found. " + f"Unexpected IPython magic {node.value.func.attr!r} found. " "Please report a bug on https://github.com/psf/black/issues." ) from None self.magics[node.value.lineno].append( From a18ee4018f855007bf4a23027a8d6478e56a36bf Mon Sep 17 00:00:00 2001 From: danieleades <33452915+danieleades@users.noreply.github.com> Date: Mon, 29 Nov 2021 02:20:52 +0000 Subject: [PATCH 059/700] add more flake8 lints (#2653) --- .pre-commit-config.yaml | 5 ++++- CHANGES.md | 1 + src/black/__init__.py | 2 +- src/black/output.py | 4 ++-- tests/optional.py | 2 +- tests/test_black.py | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3cd6639384..45810d2844a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,10 @@ repos: rev: 3.9.2 hooks: - id: flake8 - additional_dependencies: [flake8-bugbear] + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.910 diff --git a/CHANGES.md b/CHANGES.md index d81c1fd4fc4..235919ec7ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) - Fixed `match` statements with open sequence subjects, like `match a, b:` (#2639) - Fixed assignment to environment variables in Jupyter Notebooks (#2642) +- Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) ## 21.11b1 diff --git a/src/black/__init__.py b/src/black/__init__.py index aa7970cfd6c..d99c48a1b04 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -181,7 +181,7 @@ def validate_regex( @click.command( - context_settings=dict(help_option_names=["-h", "--help"]), + context_settings={"help_option_names": ["-h", "--help"]}, # While Click does set this field automatically using the docstring, mypyc # (annoyingly) strips 'em so we need to set it here too. help="The uncompromising code formatter.", diff --git a/src/black/output.py b/src/black/output.py index c85b253c159..f030d0a0d08 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -59,8 +59,8 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str: """Return a unified diff string between strings `a` and `b`.""" import difflib - a_lines = [line for line in a.splitlines(keepends=True)] - b_lines = [line for line in b.splitlines(keepends=True)] + a_lines = a.splitlines(keepends=True) + b_lines = b.splitlines(keepends=True) diff_lines = [] for line in difflib.unified_diff( a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 diff --git a/tests/optional.py b/tests/optional.py index e12b94cd29e..1cddeeaa576 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -96,7 +96,7 @@ def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS] for item in items: - all_markers_on_test = set(m.name for m in item.iter_markers()) + all_markers_on_test = {m.name for m in item.iter_markers()} optional_markers_on_test = all_markers_on_test & all_possible_optional_markers if not optional_markers_on_test or ( optional_markers_on_test & enabled_optional_markers diff --git a/tests/test_black.py b/tests/test_black.py index 4267c6110a9..51a20307e56 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1755,7 +1755,7 @@ def assert_collected_sources( report=black.Report(), stdin_filename=stdin_filename, ) - assert sorted(list(collected)) == sorted(gs_expected) + assert sorted(collected) == sorted(gs_expected) class TestFileCollection: From a066a2bc8b1b7d87b2029f5ebd684582231b0bbc Mon Sep 17 00:00:00 2001 From: Daniel Sparing Date: Mon, 29 Nov 2021 18:07:35 -0500 Subject: [PATCH 060/700] Return `NothingChanged` if non-Python cell magic is detected, to avoid tokenize error (#2630) Fixes https://github.com/psf/black/issues/2627 , a non-Python cell magic such as `%%writeline` can legitimately contain "incorrect" indentation, however this causes `tokenize-rt` to return an error. To avoid this, `validate_cell` should early detect cell magics (just like it detects `TransformerManager` transformations). Test added too, in the shape of a "badly indented" `%%writefile` within `test_non_python_magics`. Co-authored-by: Jelle Zijlstra Co-authored-by: Marco Edward Gorelli --- CHANGES.md | 3 +++ docs/faq.md | 1 + src/black/__init__.py | 7 ++++++- src/black/handle_ipynb_magics.py | 23 ++++++++--------------- tests/test_ipynb.py | 5 +++-- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 235919ec7ac..57af2c5deae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### _Black_ +- Cell magics are now only processed if they are known Python cell magics. Earlier, all + cell magics were tokenized, leading to possible indentation errors e.g. with + `%%writefile`. (#2630) - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) - Fixed `match` statements with open sequence subjects, like `match a, b:` (#2639) - Fixed assignment to environment variables in Jupyter Notebooks (#2642) diff --git a/docs/faq.md b/docs/faq.md index 72bae6b389d..88bf35b1e6a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -47,6 +47,7 @@ _Black_ is timid about formatting Jupyter Notebooks. Cells containing any of the following will not be formatted: - automagics (e.g. `pip install black`) +- non-Python cell magics (e.g. `%%writeline`) - multiline magics, e.g.: ```python diff --git a/src/black/__init__.py b/src/black/__init__.py index d99c48a1b04..c2b52e6eadb 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -57,6 +57,7 @@ remove_trailing_semicolon, put_trailing_semicolon_back, TRANSFORMED_MAGICS, + PYTHON_CELL_MAGICS, jupyter_dependencies_are_installed, ) @@ -943,7 +944,9 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo def validate_cell(src: str) -> None: - """Check that cell does not already contain TransformerManager transformations. + """Check that cell does not already contain TransformerManager transformations, + or non-Python cell magics, which might cause tokenizer_rt to break because of + indentations. If a cell contains ``!ls``, then it'll be transformed to ``get_ipython().system('ls')``. However, if the cell originally contained @@ -959,6 +962,8 @@ 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: + raise NothingChanged def format_cell(src: str, *, fast: bool, mode: Mode) -> str: diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 8ae9d2e2e6c..a0ed56baafc 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -37,20 +37,15 @@ "ESCAPED_NL", ) ) -NON_PYTHON_CELL_MAGICS = frozenset( +PYTHON_CELL_MAGICS = frozenset( ( - "bash", - "html", - "javascript", - "js", - "latex", - "markdown", - "perl", - "ruby", - "script", - "sh", - "svg", - "writefile", + "capture", + "prun", + "pypy", + "python", + "python3", + "time", + "timeit", ) ) TOKEN_HEX = secrets.token_hex @@ -230,8 +225,6 @@ def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: cell_magic_finder.visit(tree) if cell_magic_finder.cell_magic is None: return src, replacements - if cell_magic_finder.cell_magic.name in NON_PYTHON_CELL_MAGICS: - raise NothingChanged header = cell_magic_finder.cell_magic.header mask = get_token(src, header) replacements.append(Replacement(mask=mask, src=header)) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 5ecdf00b7bc..141e865815a 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -106,6 +106,7 @@ def test_magic(src: str, expected: str) -> None: ( "%%bash\n2+2", "%%html --isolated\n2+2", + "%%writefile e.txt\n meh\n meh", ), ) def test_non_python_magics(src: str) -> None: @@ -132,9 +133,9 @@ def test_magic_noop() -> None: def test_cell_magic_with_magic() -> None: - src = "%%t -n1\nls =!ls" + src = "%%timeit -n1\nls =!ls" result = format_cell(src, fast=True, mode=JUPYTER_MODE) - expected = "%%t -n1\nls = !ls" + expected = "%%timeit -n1\nls = !ls" assert result == expected From 8cdac18a04b64376e87c716cb9c2eafd182e63ff Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 30 Nov 2021 18:52:25 +0300 Subject: [PATCH 061/700] Allow top-level starred expression on match (#2659) Fixes #2647 --- CHANGES.md | 3 ++- src/blib2to3/Grammar.txt | 6 +++++- tests/data/pattern_matching_extras.py | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 57af2c5deae..4a8ee0e692c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,8 @@ cell magics were tokenized, leading to possible indentation errors e.g. with `%%writefile`. (#2630) - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) -- Fixed `match` statements with open sequence subjects, like `match a, b:` (#2639) +- Fixed `match` statements with open sequence subjects, like `match a, b:` or + `match a, *b:` (#2639) (#2659) - Fixed assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index de9a6a2283f..c3001e81065 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -238,7 +238,11 @@ yield_arg: 'from' test | testlist_star_expr # to reformat them. match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT -subject_expr: namedexpr_test (',' namedexpr_test)* [','] + +# This is more permissive than the actual version. For example it +# accepts `match *something:`, even though single-item starred expressions +# are forbidden. +subject_expr: (namedexpr_test|star_expr) (',' (namedexpr_test|star_expr))* [','] # cases case_block: "case" patterns [guard] ':' suite diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index d4bba38ee7c..706148561a2 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -43,3 +43,10 @@ def func(match: case, case: match) -> case: pass case _: pass + + +match a, *b, c: + case [*_]: + return "seq" + case {}: + return "map" From e151686c6fc291a72058a26de8e6279669d756cc Mon Sep 17 00:00:00 2001 From: Jameel Al-Aziz <247849+jalaziz@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:20:27 -0800 Subject: [PATCH 062/700] Remove hidden import from PyInstaller build (#2657) The recent 2021.4 release of pyinstaller-hooks-contrib now contains a built-in hook for platformdirs. Manually specifying the hidden import arg should no longer be needed. --- .github/workflows/upload_binary.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 8f44d4ec27b..766f37cc321 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -16,17 +16,14 @@ jobs: pathsep: ";" asset_name: black_windows.exe executable_mime: "application/vnd.microsoft.portable-executable" - platform: windows - os: ubuntu-20.04 pathsep: ":" asset_name: black_linux executable_mime: "application/x-executable" - platform: unix - os: macos-latest pathsep: ":" asset_name: black_macos executable_mime: "application/x-mach-binary" - platform: macos steps: - uses: actions/checkout@v2 @@ -43,10 +40,8 @@ jobs: python -m pip install pyinstaller - name: Build binary - run: > - python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data - 'src/blib2to3${{ matrix.pathsep }}blib2to3' --hidden-import platformdirs.${{ - matrix.platform }} src/black/__main__.py + run: | + python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py - name: Upload binary as release asset uses: actions/upload-release-asset@v1 From ebd3e391dab8d97ce7dbd837473641ddd5fb51c0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 30 Nov 2021 12:34:45 -0800 Subject: [PATCH 063/700] add FAQ entry about undetected syntax errors (#2645) This came up in #2644. --- docs/faq.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 88bf35b1e6a..0a966c99c7f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -94,7 +94,14 @@ them in the right place, this detection is not and cannot be perfect. Therefore, sometimes have to manually move these comments to the right place after you format your codebase with _Black_. -## Can I run black with PyPy? +## Can I run Black with PyPy? Yes, there is support for PyPy 3.7 and higher. You cannot format Python 2 files under PyPy, because PyPy's inbuilt ast module does not support this. + +## Why does Black not detect syntax errors in my code? + +_Black_ is an autoformatter, not a Python linter or interpreter. Detecting all syntax +errors is not a goal. It can format all code accepted by CPython (if you find an example +where that doesn't hold, please report a bug!), but it may also format some code that +CPython doesn't accept. From b336b390d0613348e6208b392e41e5512b0a85be Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 30 Nov 2021 23:56:38 +0300 Subject: [PATCH 064/700] Fix line generation for `match match:` / `case case:` (GH-2661) --- CHANGES.md | 2 ++ src/black/linegen.py | 14 ++++++++--- tests/data/pattern_matching_extras.py | 35 ++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4a8ee0e692c..85feb1a7600 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ - Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) - Fixed `match` statements with open sequence subjects, like `match a, b:` or `match a, *b:` (#2639) (#2659) +- Fixed `match`/`case` statements that contain `match`/`case` soft keywords multiple + times, like `match re.match()` (#2661) - Fixed assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4cba4164fb3..f234913a161 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -127,7 +127,7 @@ def visit_stmt( """Visit a statement. This implementation is shared for `if`, `while`, `for`, `try`, `except`, - `def`, `with`, `class`, `assert`, `match`, `case` and assignments. + `def`, `with`, `class`, `assert`, and assignments. The relevant Python language `keywords` for a given statement will be NAME leaves within it. This methods puts those on a separate line. @@ -142,6 +142,14 @@ def visit_stmt( yield from self.visit(child) + def visit_match_case(self, node: Node) -> Iterator[Line]: + """Visit either a match or case statement.""" + normalize_invisible_parens(node, parens_after=set()) + + yield from self.line() + for child in node.children: + yield from self.visit(child) + def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" if self.mode.is_pyi and is_stub_suite(node): @@ -294,8 +302,8 @@ def __post_init__(self) -> None: self.visit_decorated = self.visit_decorators # PEP 634 - self.visit_match_stmt = partial(v, keywords={"match"}, parens=Ø) - self.visit_case_block = partial(v, keywords={"case"}, parens=Ø) + self.visit_match_stmt = self.visit_match_case + self.visit_case_block = self.visit_match_case def transform_line( diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index 706148561a2..095c1a2b3bb 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -23,10 +23,10 @@ def func(match: case, case: match) -> case: match Something(): - case another: - ... case func(match, case): ... + case another: + ... match maybe, multiple: @@ -47,6 +47,33 @@ def func(match: case, case: match) -> case: match a, *b, c: case [*_]: - return "seq" + assert "seq" == _ case {}: - return "map" + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass From 5e2bb528e09df368ed7dea6b7fb9c53e799a569f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 30 Nov 2021 18:01:36 -0800 Subject: [PATCH 065/700] Reduce usage of regex (#2644) This removes all but one usage of the `regex` dependency. Tricky bits included: - A bug in test_black.py where we were incorrectly using a character range. Fix also submitted separately in #2643. - `tokenize.py` was the original use case for regex (#1047). The important bit is that we rely on `\w` to match anything valid in an identifier, and `re` fails to match a few characters as part of identifiers. My solution is to instead match all characters *except* those we know to mean something else in Python: whitespace and ASCII punctuation. This will make Black able to parse some invalid Python programs, like those that contain non-ASCII punctuation in the place of an identifier, but that seems fine to me. - One import of `regex` remains, in `trans.py`. We use a recursive regex to parse f-strings, and only `regex` supports that. I haven't thought of a better fix there (except maybe writing a manual parser), so I'm leaving that for now. My goal is to remove the `regex` dependency to reduce the risk of breakage due to dependencies and make life easier for users on platforms without wheels. --- CHANGES.md | 9 +++++---- src/black/__init__.py | 2 +- src/black/comments.py | 2 +- src/black/strings.py | 4 ++-- src/black/trans.py | 2 +- src/blib2to3/pgen2/conv.py | 2 +- src/blib2to3/pgen2/tokenize.py | 4 ++-- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 85feb1a7600..7214405c429 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,13 @@ - Cell magics are now only processed if they are known Python cell magics. Earlier, all cell magics were tokenized, leading to possible indentation errors e.g. with `%%writefile`. (#2630) -- Fixed Python 3.10 support on platforms without ProcessPoolExecutor (#2631) -- Fixed `match` statements with open sequence subjects, like `match a, b:` or +- Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631) +- Reduce usage of the `regex` dependency (#2644) +- Fix `match` statements with open sequence subjects, like `match a, b:` or `match a, *b:` (#2639) (#2659) -- Fixed `match`/`case` statements that contain `match`/`case` soft keywords multiple +- Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple times, like `match re.match()` (#2661) -- Fixed assignment to environment variables in Jupyter Notebooks (#2642) +- Fix assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) ## 21.11b1 diff --git a/src/black/__init__.py b/src/black/__init__.py index c2b52e6eadb..1923c069ede 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -10,7 +10,7 @@ import os from pathlib import Path from pathspec.patterns.gitwildmatch import GitWildMatchPatternError -import regex as re +import re import signal import sys import tokenize diff --git a/src/black/comments.py b/src/black/comments.py index a8152d687a3..28b9117101d 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,7 +1,7 @@ import sys from dataclasses import dataclass from functools import lru_cache -import regex as re +import re from typing import Iterator, List, Optional, Union if sys.version_info >= (3, 8): diff --git a/src/black/strings.py b/src/black/strings.py index 97debe3b5de..06a5da01f0c 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -2,7 +2,7 @@ Simple formatting on strings. Further string formatting code is in trans.py. """ -import regex as re +import re import sys from functools import lru_cache from typing import List, Pattern @@ -156,7 +156,7 @@ def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str: # performance on a long list literal of strings by 5-9% since lru_cache's # caching overhead is much lower. @lru_cache(maxsize=64) -def _cached_compile(pattern: str) -> re.Pattern: +def _cached_compile(pattern: str) -> Pattern[str]: return re.compile(pattern) diff --git a/src/black/trans.py b/src/black/trans.py index d918ef111a2..a4d1e6fbc79 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass -import regex as re +import regex as re # We need recursive patterns here (?R) from typing import ( Any, Callable, diff --git a/src/blib2to3/pgen2/conv.py b/src/blib2to3/pgen2/conv.py index 78165217a1b..fa9825e54d6 100644 --- a/src/blib2to3/pgen2/conv.py +++ b/src/blib2to3/pgen2/conv.py @@ -29,7 +29,7 @@ """ # Python imports -import regex as re +import re # Local imports from pgen2 import grammar, token diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 283fac2d537..a7e17df1e8f 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -52,7 +52,7 @@ __author__ = "Ka-Ping Yee " __credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro" -import regex as re +import re from codecs import BOM_UTF8, lookup from blib2to3.pgen2.token import * @@ -86,7 +86,7 @@ def _combinations(*l): Comment = r"#[^\r\n]*" Ignore = Whitespace + any(r"\\\r?\n" + Whitespace) + maybe(Comment) Name = ( # this is invalid but it's fine because Name comes after Number in all groups - r"\w+" + r"[^\s#\(\)\[\]\{\}+\-*/!@$%^&=|;:'\",\.<>/?`~\\]+" ) Binnumber = r"0[bB]_?[01]+(?:_[01]+)*" From 0f7cf9187f9c9644565570a67a66f690f8f2bfbb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 30 Nov 2021 18:39:39 -0800 Subject: [PATCH 066/700] fix error message for match (#2649) Fixes #2648. Co-authored-by: Batuhan Taskaya --- CHANGES.md | 1 + src/black/parsing.py | 6 ++++-- tests/data/pattern_matching_invalid.py | 18 ++++++++++++++++++ tests/test_format.py | 9 +++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/data/pattern_matching_invalid.py diff --git a/CHANGES.md b/CHANGES.md index 7214405c429..c0cf60af98a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ times, like `match re.match()` (#2661) - Fix assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) +- Fix parser error location on invalid syntax in a `match` statement (#2649) ## 21.11b1 diff --git a/src/black/parsing.py b/src/black/parsing.py index 32cfa5239f1..e38405637cd 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -75,8 +75,10 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: # 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): - # Python 3.7+ + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 grammars.append( pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords ) diff --git a/tests/data/pattern_matching_invalid.py b/tests/data/pattern_matching_invalid.py new file mode 100644 index 00000000000..22b5b94c0a4 --- /dev/null +++ b/tests/data/pattern_matching_invalid.py @@ -0,0 +1,18 @@ +# First match, no errors +match something: + case bla(): + pass + +# Problem on line 10 +match invalid_case: + case valid_case: + pass + case a := b: + pass + case valid_case: + pass + +# No problems either +match something: + case bla(): + pass diff --git a/tests/test_format.py b/tests/test_format.py index 8f8ffb3610e..f97d7165b1a 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -200,6 +200,15 @@ def test_python_310(filename: str) -> None: 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}) + with pytest.raises(black.parsing.InvalidInput) as exc_info: + assert_format(source, expected, mode, minimum_version=(3, 10)) + + exc_info.match("Cannot parse: 10:11") + + def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" source, expected = read_data("docstring_no_string_normalization") From f1813e31b6deed0901c8a7fb1f102b9af53de351 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 1 Dec 2021 09:52:24 -0800 Subject: [PATCH 067/700] Fix determination of f-string expression spans (#2654) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/trans.py | 72 ++++++++++++++++++++++++++++++++++----------- tests/test_trans.py | 50 +++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 tests/test_trans.py diff --git a/CHANGES.md b/CHANGES.md index c0cf60af98a..5b648225ad7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ times, like `match re.match()` (#2661) - Fix assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) +- Fix determination of f-string expression spans (#2654) - Fix parser error location on invalid syntax in a `match` statement (#2649) ## 21.11b1 diff --git a/src/black/trans.py b/src/black/trans.py index a4d1e6fbc79..6aca3a8733f 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -942,6 +942,57 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: return max_string_length +def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]: + """ + Yields spans corresponding to expressions in a given f-string. + Spans are half-open ranges (left inclusive, right exclusive). + Assumes the input string is a valid f-string, but will not crash if the input + string is invalid. + """ + stack: List[int] = [] # our curly paren stack + i = 0 + while i < len(s): + if s[i] == "{": + # if we're in a string part of the f-string, ignore escaped curly braces + if not stack and i + 1 < len(s) and s[i + 1] == "{": + i += 2 + continue + stack.append(i) + i += 1 + continue + + if s[i] == "}": + if not stack: + i += 1 + continue + j = stack.pop() + # we've made it back out of the expression! yield the span + if not stack: + yield (j, i + 1) + i += 1 + continue + + # if we're in an expression part of the f-string, fast forward through strings + # note that backslashes are not legal in the expression portion of f-strings + if stack: + delim = None + if s[i : i + 3] in ("'''", '"""'): + delim = s[i : i + 3] + elif s[i] in ("'", '"'): + delim = s[i] + if delim: + i += len(delim) + while i < len(s) and s[i : i + len(delim)] != delim: + i += 1 + i += len(delim) + continue + i += 1 + + +def fstring_contains_expr(s: str) -> bool: + return any(iter_fexpr_spans(s)) + + class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): """ StringTransformer that splits "atom" strings (i.e. strings which exist on @@ -981,17 +1032,6 @@ class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): """ MIN_SUBSTR_SIZE: Final = 6 - # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. - RE_FEXPR: Final = r""" - (? TMatchResult: LL = line.leaves @@ -1058,8 +1098,8 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: # contain any f-expressions, but ONLY if the original f-string # contains at least one f-expression. Otherwise, we will alter the AST # of the program. - drop_pointless_f_prefix = ("f" in prefix) and re.search( - self.RE_FEXPR, LL[string_idx].value, re.VERBOSE + drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr( + LL[string_idx].value ) first_string_line = True @@ -1299,9 +1339,7 @@ def _iter_fexpr_slices(self, string: str) -> Iterator[Tuple[Index, Index]]: """ if "f" not in get_string_prefix(string).lower(): return - - for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE): - yield match.span() + yield from iter_fexpr_spans(string) def _get_illegal_split_indices(self, string: str) -> Set[Index]: illegal_indices: Set[Index] = set() @@ -1417,7 +1455,7 @@ def _normalize_f_string(self, string: str, prefix: str) -> str: """ assert_is_leaf_string(string) - if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE): + if "f" in prefix and not fstring_contains_expr(string): new_prefix = prefix.replace("f", "") temp = string[len(prefix) :] diff --git a/tests/test_trans.py b/tests/test_trans.py new file mode 100644 index 00000000000..a1666a9c166 --- /dev/null +++ b/tests/test_trans.py @@ -0,0 +1,50 @@ +from typing import List, Tuple +from black.trans import iter_fexpr_spans + + +def test_fexpr_spans() -> None: + def check( + string: str, expected_spans: List[Tuple[int, int]], expected_slices: List[str] + ) -> None: + spans = list(iter_fexpr_spans(string)) + + # Checking slices isn't strictly necessary, but it's easier to verify at + # a glance than only spans + assert len(spans) == len(expected_slices) + for (i, j), slice in zip(spans, expected_slices): + assert len(string[i:j]) == j - i + assert string[i:j] == slice + + assert spans == expected_spans + + # Most of these test cases omit the leading 'f' and leading / closing quotes + # for convenience + # Some additional property-based tests can be found in + # https://github.com/psf/black/pull/2654#issuecomment-981411748 + check("""{var}""", [(0, 5)], ["{var}"]) + check("""f'{var}'""", [(2, 7)], ["{var}"]) + check("""f'{1 + f() + 2 + "asdf"}'""", [(2, 24)], ["""{1 + f() + 2 + "asdf"}"""]) + check("""text {var} text""", [(5, 10)], ["{var}"]) + check("""text {{ {var} }} text""", [(8, 13)], ["{var}"]) + check("""{a} {b} {c}""", [(0, 3), (4, 7), (8, 11)], ["{a}", "{b}", "{c}"]) + check("""f'{a} {b} {c}'""", [(2, 5), (6, 9), (10, 13)], ["{a}", "{b}", "{c}"]) + check("""{ {} }""", [(0, 6)], ["{ {} }"]) + check("""{ {{}} }""", [(0, 8)], ["{ {{}} }"]) + check("""{ {{{}}} }""", [(0, 10)], ["{ {{{}}} }"]) + check("""{{ {{{}}} }}""", [(5, 7)], ["{}"]) + check("""{{ {{{var}}} }}""", [(5, 10)], ["{var}"]) + check("""{f"{0}"}""", [(0, 8)], ["""{f"{0}"}"""]) + check("""{"'"}""", [(0, 5)], ["""{"'"}"""]) + check("""{"{"}""", [(0, 5)], ["""{"{"}"""]) + check("""{"}"}""", [(0, 5)], ["""{"}"}"""]) + check("""{"{{"}""", [(0, 6)], ["""{"{{"}"""]) + check("""{''' '''}""", [(0, 9)], ["""{''' '''}"""]) + check("""{'''{'''}""", [(0, 9)], ["""{'''{'''}"""]) + check("""{''' {'{ '''}""", [(0, 13)], ["""{''' {'{ '''}"""]) + check( + '''f\'\'\'-{f"""*{f"+{f'.{x}.'}+"}*"""}-'y\\'\'\'\'''', + [(5, 33)], + ['''{f"""*{f"+{f'.{x}.'}+"}*"""}'''], + ) + check(r"""{}{""", [(0, 2)], ["{}"]) + check("""f"{'{'''''''''}\"""", [(2, 15)], ["{'{'''''''''}"]) From 84851914488b2f3f6388a0760ee04306e4c2fc18 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 1 Dec 2021 13:47:33 -0800 Subject: [PATCH 068/700] slightly better example link (#2617) Since we also need to update two places in the docs --- docs/contributing/release_process.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 718ea3dc9a2..9ee7dbc607c 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -8,8 +8,8 @@ explain what everything does and how to release _Black_ using said automation. To cut a release, you must be a _Black_ maintainer with `GitHub Release` creation access. Using this access, the release process is: -1. Cut a new PR editing `CHANGES.md` to version the latest changes - 1. Example PR: https://github.com/psf/black/pull/2192 +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` 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 From b0c2bcc9537d238c8a580294ecbc41de465d7f55 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:05:59 -0500 Subject: [PATCH 069/700] Treat functions/classes in blocks as if they're nested (GH-2472) * Treat functions/classes in blocks as if they're nested One curveball is that we still want two preceding newlines before blocks that are probably logically disconnected. In other words: if condition: def foo(): return "hi" # <- aside: this is the goal of this commit else: def foo(): return "cya" # <- the two newlines spacing here should stay # since this probably isn't related with open("db.json", encoding="utf-8") as f: data = f.read() Unfortunately that means we have to special case specific clause types instead of just being able to just for a colon leaf. The hack used here is to check whether we're adding preceding newlines for a standalone or dependent clause. "Standalone" being a clause that doesn't need another clause to be valid (eg. if) and vice versa. Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/lines.py | 24 ++++++++++++-- src/black_primer/primer.json | 2 +- tests/data/function2.py | 63 ++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5b648225ad7..59042914174 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) - Fix determination of f-string expression spans (#2654) - Fix parser error location on invalid syntax in a `match` statement (#2649) +- Functions and classes in blocks now have more consistent surrounding spacing (#2472) ## 21.11b1 diff --git a/src/black/lines.py b/src/black/lines.py index 63225c0e6d3..f2bdada008a 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -447,11 +447,31 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 0 depth = current_line.depth while self.previous_defs and self.previous_defs[-1] >= depth: - self.previous_defs.pop() if self.is_pyi: before = 0 if depth else 1 else: - before = 1 if depth else 2 + if depth: + before = 1 + elif ( + not depth + and self.previous_defs[-1] + and current_line.leaves[-1].type == token.COLON + and ( + current_line.leaves[0].value + not in ("with", "try", "for", "while", "if", "match") + ) + ): + # We shouldn't add two newlines between an indented function and + # a dependent non-indented clause. This is to avoid issues with + # conditional function definitions that are technically top-level + # and therefore get two trailing newlines, but look weird and + # inconsistent when they're followed by elif, else, etc. This is + # worse because these functions only get *one* preceding newline + # already. + before = 1 + else: + before = 2 + self.previous_defs.pop() if current_line.is_decorator or current_line.is_def or current_line.is_class: return self._maybe_empty_lines_for_class_or_def(current_line, before) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 2290d1df005..8fe61e889f8 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -116,7 +116,7 @@ }, "pyanalyze": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/quora/pyanalyze.git", "long_checkout": false, "py_versions": ["all"] diff --git a/tests/data/function2.py b/tests/data/function2.py index cfc259ea7bd..5bb36c26318 100644 --- a/tests/data/function2.py +++ b/tests/data/function2.py @@ -23,6 +23,35 @@ def inner(): pass print("Inner defs should breathe a little.") + +if os.name == "posix": + import termios + def i_should_be_followed_by_only_one_newline(): + pass +elif os.name == "nt": + try: + import msvcrt + def i_should_be_followed_by_only_one_newline(): + pass + + except ImportError: + + def i_should_be_followed_by_only_one_newline(): + pass + +elif False: + + class IHopeYouAreHavingALovelyDay: + def __call__(self): + print("i_should_be_followed_by_only_one_newline") +else: + + def foo(): + pass + +with hmm_but_this_should_get_two_preceding_newlines(): + pass + # output def f( @@ -56,3 +85,37 @@ def inner(): pass print("Inner defs should breathe a little.") + + +if os.name == "posix": + import termios + + def i_should_be_followed_by_only_one_newline(): + pass + +elif os.name == "nt": + try: + import msvcrt + + def i_should_be_followed_by_only_one_newline(): + pass + + except ImportError: + + def i_should_be_followed_by_only_one_newline(): + pass + +elif False: + + class IHopeYouAreHavingALovelyDay: + def __call__(self): + print("i_should_be_followed_by_only_one_newline") + +else: + + def foo(): + pass + + +with hmm_but_this_should_get_two_preceding_newlines(): + pass From 20d7ae0676be4931d0b2e6d4a6a0877070264d13 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 2 Dec 2021 20:58:22 +0300 Subject: [PATCH 070/700] Ensure match/case are recognized as statements (#2665) --- CHANGES.md | 1 + src/black/nodes.py | 2 ++ tests/data/pattern_matching_style.py | 27 +++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 31 insertions(+) create mode 100644 tests/data/pattern_matching_style.py diff --git a/CHANGES.md b/CHANGES.md index 59042914174..c9a4f09a72a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ `match a, *b:` (#2639) (#2659) - Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple times, like `match re.match()` (#2661) +- Fix `case` statements with an inline body (#2665) - Fix assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) - Fix determination of f-string expression spans (#2654) diff --git a/src/black/nodes.py b/src/black/nodes.py index 36dd1890511..437051d3f6d 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -52,6 +52,8 @@ syms.with_stmt, syms.funcdef, syms.classdef, + syms.match_stmt, + syms.case_block, } STANDALONE_COMMENT: Final = 153 token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT" diff --git a/tests/data/pattern_matching_style.py b/tests/data/pattern_matching_style.py new file mode 100644 index 00000000000..c1c0aeedb70 --- /dev/null +++ b/tests/data/pattern_matching_style.py @@ -0,0 +1,27 @@ +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): print(2) + case a: pass + +# output + +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(2) + case a: + pass diff --git a/tests/test_format.py b/tests/test_format.py index f97d7165b1a..d44be1e8712 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -74,6 +74,7 @@ "pattern_matching_simple", "pattern_matching_complex", "pattern_matching_extras", + "pattern_matching_style", "parenthesized_context_managers", ] From bd9d52b52d58df60bffe164309a48cb61ac8d3b7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Dec 2021 14:35:02 -0800 Subject: [PATCH 071/700] Remove regex dependency (GH-2663) We were no longer using it since GH-2644 and GH-2654. This should hopefully make using Black easier to use as there's one less compiled dependency. The core team also doesn't have to deal with the surprisingly frequent fires the regex packaging setup goes through. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 2 +- Pipfile | 1 - Pipfile.lock | 112 +---------------------------------- docs/integrations/editors.md | 28 ++++----- setup.py | 1 - src/black/trans.py | 4 +- 6 files changed, 17 insertions(+), 131 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c9a4f09a72a..fc198a8f8c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ cell magics were tokenized, leading to possible indentation errors e.g. with `%%writefile`. (#2630) - Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631) -- Reduce usage of the `regex` dependency (#2644) +- Remove dependency on `regex` (#2644) (#2663) - Fix `match` statements with open sequence subjects, like `match a, b:` or `match a, *b:` (#2639) (#2659) - Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple diff --git a/Pipfile b/Pipfile index 9608f4d4ef7..a3af5fd8844 100644 --- a/Pipfile +++ b/Pipfile @@ -42,7 +42,6 @@ platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" -regex = ">=2021.4.4" tomli = ">=0.2.6, <2.0.0" typed-ast = "==1.4.3" typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} diff --git a/Pipfile.lock b/Pipfile.lock index a02ea4a2590..b2a9f6c6fc0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a516705ed9270469fd58d20f1b26a94a6ed052451ef7425d82605b80513a65b3" + "sha256": "7728caac52b47ed119a804ead88afa002d62c17a324e962b7833b8944049609b" }, "pipfile-spec": 6, "requires": {}, @@ -375,61 +375,6 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, - "regex": { - "hashes": [ - "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", - "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", - "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", - "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", - "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", - "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", - "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", - "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", - "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", - "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", - "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", - "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", - "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", - "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", - "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", - "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", - "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", - "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", - "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", - "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", - "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", - "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", - "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", - "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", - "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", - "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", - "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", - "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", - "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", - "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", - "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", - "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", - "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", - "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", - "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", - "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", - "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", - "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", - "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", - "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", - "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", - "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", - "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", - "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", - "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", - "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", - "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", - "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", - "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" - ], - "index": "pypi", - "version": "==2021.11.10" - }, "setuptools": { "hashes": [ "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf", @@ -1624,61 +1569,6 @@ "index": "pypi", "version": "==30.0" }, - "regex": { - "hashes": [ - "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", - "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", - "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", - "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", - "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", - "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", - "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", - "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", - "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", - "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", - "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", - "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", - "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", - "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", - "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", - "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", - "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", - "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", - "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", - "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", - "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", - "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", - "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", - "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", - "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", - "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", - "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", - "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", - "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", - "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", - "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", - "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", - "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", - "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", - "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", - "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", - "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", - "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", - "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", - "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", - "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", - "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", - "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", - "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", - "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", - "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", - "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", - "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", - "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" - ], - "index": "pypi", - "version": "==2021.11.10" - }, "requests": { "hashes": [ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index d3be7c0ea84..9c279564fa3 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -204,30 +204,28 @@ Traceback (most recent call last): ImportError: /home/gui/.vim/black/lib/python3.7/site-packages/typed_ast/_ast3.cpython-37m-x86_64-linux-gnu.so: undefined symbool: PyExc_KeyboardInterrupt ``` -Then you need to install `typed_ast` and `regex` directly from the source code. The -error happens because `pip` will download [Python wheels](https://pythonwheels.com/) if -they are available. Python wheels are a new standard of distributing Python packages and -packages that have Cython and extensions written in C are already compiled, so the -installation is much more faster. The problem here is that somehow the Python -environment inside Vim does not match with those already compiled C extensions and these -kind of errors are the result. Luckily there is an easy fix: installing the packages -from the source code. - -The two packages that cause the problem are: - -- [regex](https://pypi.org/project/regex/) +Then you need to install `typed_ast` directly from the source code. The error happens +because `pip` will download [Python wheels](https://pythonwheels.com/) if they are +available. Python wheels are a new standard of distributing Python packages and packages +that have Cython and extensions written in C are already compiled, so the installation +is much more faster. The problem here is that somehow the Python environment inside Vim +does not match with those already compiled C extensions and these kind of errors are the +result. Luckily there is an easy fix: installing the packages from the source code. + +The package that causes problems is: + - [typed-ast](https://pypi.org/project/typed-ast/) Now remove those two packages: ```console -$ pip uninstall regex typed-ast -y +$ pip uninstall typed-ast -y ``` And now you can install them with: ```console -$ pip install --no-binary :all: regex typed-ast +$ pip install --no-binary :all: typed-ast ``` The C extensions will be compiled and now Vim's Python environment will match. Note that @@ -237,7 +235,7 @@ Ubuntu/Debian do `sudo apt-get install build-essential python3-dev`). If you later want to update _Black_, you should do it like this: ```console -$ pip install -U black --no-binary regex,typed-ast +$ pip install -U black --no-binary typed-ast ``` ### With ALE diff --git a/setup.py b/setup.py index 33b7239a13e..a21bc87264d 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,6 @@ def find_python_files(base: Path) -> List[Path]: "platformdirs>=2", "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", - "regex>=2021.4.4", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.10.0.0", diff --git a/src/black/trans.py b/src/black/trans.py index 6aca3a8733f..cb41c1be487 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass -import regex as re # We need recursive patterns here (?R) +import re from typing import ( Any, Callable, @@ -453,7 +453,7 @@ def make_naked(string: str, string_prefix: str) -> str: # with 'f'... if "f" in prefix and "f" not in next_prefix: # Then we must escape any braces contained in this substring. - SS = re.subf(r"(\{|\})", "{1}{1}", SS) + SS = re.sub(r"(\{|\})", r"\1\1", SS) NSS = make_naked(SS, next_prefix) From 136930fccb99320865622e12ffc21bdd45fd7501 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 3 Dec 2021 17:49:33 +0300 Subject: [PATCH 072/700] Make star-expression spacing consistent in match/case (#2667) --- CHANGES.md | 1 + src/black/nodes.py | 2 ++ tests/data/pattern_matching_extras.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index fc198a8f8c0..e5f4a1fdf82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ - Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple times, like `match re.match()` (#2661) - Fix `case` statements with an inline body (#2665) +- Fix styling of starred expressions inside `match` subject (#2667) - Fix assignment to environment variables in Jupyter Notebooks (#2642) - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) - Fix determination of f-string expression spans (#2654) diff --git a/src/black/nodes.py b/src/black/nodes.py index 437051d3f6d..8bf1934bc2a 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -97,6 +97,8 @@ syms.listmaker, syms.testlist_gexp, syms.testlist_star_expr, + syms.subject_expr, + syms.pattern, } TEST_DESCENDANTS: Final = { syms.test, diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index 095c1a2b3bb..60ad8a3d81b 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -77,3 +77,8 @@ def func(match: case, case: match) -> case: match match: case case: pass + + +match a, *b(), c: + case d, *f, g: + pass From f52cb0fe3775829245acfeae191e8d63120c8416 Mon Sep 17 00:00:00 2001 From: Tanvi Moharir <74228962+tanvimoharir@users.noreply.github.com> Date: Sun, 5 Dec 2021 01:51:26 +0530 Subject: [PATCH 073/700] Don't let TokenError bubble up from lib2to3_parse (GH-2343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error: cannot format : ('EOF in multi-line statement', (2, 0)) ▲ before ▼ after error: cannot format : Cannot parse: 2:0: EOF in multi-line statement Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + src/black/parsing.py | 7 +++++++ tests/test_black.py | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e5f4a1fdf82..5d89c71f580 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ - Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) - Fix determination of f-string expression spans (#2654) - Fix parser error location on invalid syntax in a `match` statement (#2649) +- 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) ## 21.11b1 diff --git a/src/black/parsing.py b/src/black/parsing.py index e38405637cd..b673027022f 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -17,6 +17,7 @@ from blib2to3.pgen2 import driver from blib2to3.pgen2.grammar import Grammar from blib2to3.pgen2.parse import ParseError +from blib2to3.pgen2.tokenize import TokenError from black.mode import TargetVersion, Feature, supports_feature from black.nodes import syms @@ -109,6 +110,12 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - except IndexError: faulty_line = "" exc = 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]}") + else: raise exc from None diff --git a/tests/test_black.py b/tests/test_black.py index 51a20307e56..92598532e2c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1557,6 +1557,15 @@ def test_code_option_parent_config(self) -> None: call_args[0].lower() == str(pyproject_path).lower() ), "Incorrect config loaded." + def test_for_handled_unexpected_eof_error(self) -> None: + """ + Test that an unexpected EOF SyntaxError is nicely presented. + """ + with pytest.raises(black.parsing.InvalidInput) as exc_info: + black.lib2to3_parse("print(", {}) + + exc_info.match("Cannot parse: 2:0: EOF in multi-line statement") + class TestCaching: def test_cache_broken_file(self) -> None: From dc8cdda8fdd6941103240ae3279034d2acdc69bc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 4 Dec 2021 15:30:23 -0800 Subject: [PATCH 074/700] tell users to use -t py310 (#2668) --- CHANGES.md | 1 + src/black/parsing.py | 24 +++++++++++++++++++++++- tests/test_format.py | 9 +++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5d89c71f580..dcb51f7e98a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Point users to using `--target-version py310` if we detect 3.10-only syntax (#2668) - Cell magics are now only processed if they are known Python cell magics. Earlier, all cell magics were tokenized, leading to possible indentation errors e.g. with `%%writefile`. (#2630) diff --git a/src/black/parsing.py b/src/black/parsing.py index b673027022f..2fd41f03ecd 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -43,6 +43,11 @@ ast3 = ast27 = ast +PY310_HINT: Final[ + str +] = "Consider using --target-version py310 to parse Python 3.10 code." + + class InvalidInput(ValueError): """Raised when input source code fails all parse attempts.""" @@ -96,7 +101,8 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - if not src_txt.endswith("\n"): src_txt += "\n" - for grammar in get_grammars(set(target_versions)): + grammars = get_grammars(set(target_versions)) + for grammar in grammars: drv = driver.Driver(grammar) try: result = drv.parse_string(src_txt, True) @@ -117,6 +123,12 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - exc = 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 raise exc from None if isinstance(result, Leaf): @@ -124,6 +136,16 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - return result +def matches_grammar(src_txt: str, grammar: Grammar) -> bool: + drv = driver.Driver(grammar) + try: + drv.parse_string(src_txt, True) + except ParseError: + return False + else: + return True + + def lib2to3_unparse(node: Node) -> str: """Given a lib2to3 node, return its string representation.""" code = str(node) diff --git a/tests/test_format.py b/tests/test_format.py index d44be1e8712..30099aaf1bc 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -210,6 +210,15 @@ 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_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" source, expected = read_data("docstring_no_string_normalization") From 9424e795bf662743da6423b74e30ab74d1a93775 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 4 Dec 2021 15:57:40 -0800 Subject: [PATCH 075/700] Reorganize changelog (#2669) I believe it would be useful to split up the long list of changes a bit more. Specific changes: - Removed the entry for new flake8 plugins; this is purely internal and not of interest to users - Put regex in the packaging section - New section for Jupyter Notebook - New section for Python 3.10, mostly match/case stuff --- CHANGES.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dcb51f7e98a..097725ec0be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,24 +4,32 @@ ### _Black_ -- Point users to using `--target-version py310` if we detect 3.10-only syntax (#2668) +- 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) + +#### Jupyter Notebook support + - Cell magics are now only processed if they are known Python cell magics. Earlier, all cell magics were tokenized, leading to possible indentation errors e.g. with `%%writefile`. (#2630) -- Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631) -- Remove dependency on `regex` (#2644) (#2663) +- Fix assignment to environment variables in Jupyter Notebooks (#2642) + +#### Python 3.10 support + +- Point users to using `--target-version py310` if we detect 3.10-only syntax (#2668) - Fix `match` statements with open sequence subjects, like `match a, b:` or `match a, *b:` (#2639) (#2659) - Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple times, like `match re.match()` (#2661) - Fix `case` statements with an inline body (#2665) - Fix styling of starred expressions inside `match` subject (#2667) -- Fix assignment to environment variables in Jupyter Notebooks (#2642) -- Add `flake8-simplify` and `flake8-comprehensions` plugins (#2653) -- Fix determination of f-string expression spans (#2654) - Fix parser error location on invalid syntax in a `match` statement (#2649) -- 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) +- Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631) + +### Packaging + +- Remove dependency on `regex` (#2644) (#2663) ## 21.11b1 From d9eee31ec81c42d9953aee0d7f0adaf211519a10 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:53:58 -0500 Subject: [PATCH 076/700] blib2to3 can raise TokenError and IndentationError too (#2671) --- src/black/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/parsing.py b/src/black/parsing.py index 2fd41f03ecd..c101643fe11 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -140,7 +140,7 @@ def matches_grammar(src_txt: str, grammar: Grammar) -> bool: drv = driver.Driver(grammar) try: drv.parse_string(src_txt, True) - except ParseError: + except (ParseError, TokenError, IndentationError): return False else: return True From 28ab82aab013978b7ed91bda816de3d41385f260 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 6 Dec 2021 00:03:48 +0300 Subject: [PATCH 077/700] perf: drop the initial stack copy (#2670) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 2 ++ src/blib2to3/pgen2/parse.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 097725ec0be..434f80980a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - Fix styling of starred expressions inside `match` subject (#2667) - Fix parser error location on invalid syntax in a `match` statement (#2649) - Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631) +- Improve parsing performance on code that uses `match` under `--target-version py310` + up to ~50% (#2670) ### Packaging diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 792e8e66698..e5dad3ae766 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -53,7 +53,7 @@ def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> No self.context = context # not really matter self._dead_ilabels: Set[int] = set() - self._start_point = copy.deepcopy(self.parser.stack) + self._start_point = self.parser.stack self._points = {ilabel: copy.deepcopy(self._start_point) for ilabel in ilabels} @property From f1d4e742c91dd5179d742b0db9293c4472b765f8 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 5 Dec 2021 16:39:34 -0500 Subject: [PATCH 078/700] Prepare for release 21.12b0 (GH-2673) Let's do this! --- 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 434f80980a5..9e13ef438cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.12b0 ### _Black_ diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 2149027218d..9c53f30687d 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.11b1 + rev: 21.12b0 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 3dc26b7013f..d002ff0173a 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.11b1 +black, version 21.12b0 ``` An option to require a specific version to be running is also provided. From 085efac037c07ef299edbf48a4d871f17b296743 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 5 Dec 2021 15:47:53 -0800 Subject: [PATCH 079/700] no longer expect changes on pyanalyze (#2674) https://github.com/quora/pyanalyze/pull/316 --- src/black_primer/primer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 8fe61e889f8..2290d1df005 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -116,7 +116,7 @@ }, "pyanalyze": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/quora/pyanalyze.git", "long_checkout": false, "py_versions": ["all"] From e7ddf524b056d2bc42ee6b2b5c3314e0dd5d95fb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Dec 2021 19:13:05 -0800 Subject: [PATCH 080/700] Show details when a regex fails to compile (GH-2678) --- CHANGES.md | 6 ++++++ src/black/__init__.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9e13ef438cb..37248202750 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Improve error message for invalid regular expression (#2678) + ## 21.12b0 ### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 1923c069ede..e2376c45617 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -177,8 +177,8 @@ def validate_regex( ) -> Optional[Pattern[str]]: try: return re_compile_maybe_verbose(value) if value is not None else None - except re.error: - raise click.BadParameter("Not a valid regular expression") from None + except re.error as e: + raise click.BadParameter(f"Not a valid regular expression: {e}") from None @click.command( From 1c6b3a3a6fbc50b651d4ac34247903041d3f6329 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 13 Dec 2021 00:10:22 +0300 Subject: [PATCH 081/700] Support as-expressions on dict items (GH-2686) --- CHANGES.md | 2 ++ src/blib2to3/Grammar.txt | 4 ++-- tests/data/pattern_matching_extras.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 37248202750..0dcf35ea199 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ### _Black_ - Improve error message for invalid regular expression (#2678) +- Fix mapping cases that contain as-expressions, like `case {"key": 1 | 2 as password}` + (#2686) ## 21.12b0 diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index c3001e81065..600712ce2f0 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -168,8 +168,8 @@ subscript: test [':=' test] | [test] ':' [test] [sliceop] sliceop: ':' [test] exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] testlist: test (',' test)* [','] -dictsetmaker: ( ((test ':' test | '**' expr) - (comp_for | (',' (test ':' test | '**' expr))* [','])) | +dictsetmaker: ( ((test ':' asexpr_test | '**' expr) + (comp_for | (',' (test ':' asexpr_test | '**' expr))* [','])) | ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index 60ad8a3d81b..c00585e9285 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -82,3 +82,13 @@ def func(match: case, case: match) -> case: match a, *b(), c: case d, *f, g: pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass From ab8651371075ced6f58f519e48fc4e8ac529e8ce Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 15 Dec 2021 02:22:56 +0300 Subject: [PATCH 082/700] `from __future__ import annotations` now implies 3.7+ (#2690) --- CHANGES.md | 1 + src/black/__init__.py | 22 +++++++++++++++++----- src/black/mode.py | 19 +++++++++++++++++++ tests/test_black.py | 18 ++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0dcf35ea199..87e36f4dbe7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - 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 diff --git a/src/black/__init__.py b/src/black/__init__.py index e2376c45617..59018d00de4 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -40,7 +40,7 @@ from black.lines import Line, EmptyLineTracker from black.linegen import transform_line, LineGenerator, LN from black.comments import normalize_fmt_off -from black.mode import Mode, TargetVersion +from black.mode import FUTURE_FLAG_TO_FEATURE, Mode, TargetVersion from black.mode import Feature, supports_feature, VERSION_TO_FEATURES from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache from black.concurrency import cancel, shutdown, maybe_install_uvloop @@ -1080,7 +1080,7 @@ def f( if mode.target_versions: versions = mode.target_versions else: - versions = detect_target_versions(src_node) + versions = detect_target_versions(src_node, future_imports=future_imports) # TODO: fully drop support and this code hopefully in January 2022 :D if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}: @@ -1132,7 +1132,9 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: return tiow.read(), encoding, newline -def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 +def get_features_used( # noqa: C901 + node: Node, *, future_imports: Optional[Set[str]] = None +) -> Set[Feature]: """Return a set of (relatively) new Python features used in this file. Currently looking for: @@ -1142,9 +1144,17 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 - positional only arguments in function signatures and lambdas; - assignment expression; - relaxed decorator syntax; + - usage of __future__ flags (annotations); - print / exec statements; """ features: Set[Feature] = set() + if future_imports: + features |= { + FUTURE_FLAG_TO_FEATURE[future_import] + for future_import in future_imports + if future_import in FUTURE_FLAG_TO_FEATURE + } + for n in node.pre_order(): if n.type == token.STRING: value_head = n.value[:2] # type: ignore @@ -1229,9 +1239,11 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901 return features -def detect_target_versions(node: Node) -> Set[TargetVersion]: +def detect_target_versions( + node: Node, *, future_imports: Optional[Set[str]] = None +) -> Set[TargetVersion]: """Detect the version to target based on the nodes used.""" - features = get_features_used(node) + features = get_features_used(node, future_imports=future_imports) return { version for version in TargetVersion if features <= VERSION_TO_FEATURES[version] } diff --git a/src/black/mode.py b/src/black/mode.py index e2417531240..a2b7d9e9e2d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -4,11 +4,18 @@ chosen by the user. """ +import sys + from dataclasses import dataclass, field from enum import Enum from operator import attrgetter from typing import Dict, Set +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + from black.const import DEFAULT_LINE_LENGTH @@ -44,6 +51,9 @@ class Feature(Enum): PATTERN_MATCHING = 11 FORCE_OPTIONAL_PARENTHESES = 50 + # __future__ flags + FUTURE_ANNOTATIONS = 51 + # temporary for Python 2 deprecation PRINT_STMT = 200 EXEC_STMT = 201 @@ -55,6 +65,11 @@ class Feature(Enum): BACKQUOTE_REPR = 207 +FUTURE_FLAG_TO_FEATURE: Final = { + "annotations": Feature.FUTURE_ANNOTATIONS, +} + + VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { TargetVersion.PY27: { Feature.ASYNC_IDENTIFIERS, @@ -89,6 +104,7 @@ class Feature(Enum): Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, }, TargetVersion.PY38: { Feature.UNICODE_LITERALS, @@ -97,6 +113,7 @@ class Feature(Enum): Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, Feature.ASSIGNMENT_EXPRESSIONS, Feature.POS_ONLY_ARGUMENTS, }, @@ -107,6 +124,7 @@ class Feature(Enum): Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, Feature.ASSIGNMENT_EXPRESSIONS, Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, @@ -118,6 +136,7 @@ class Feature(Enum): Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, Feature.ASSIGNMENT_EXPRESSIONS, Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, diff --git a/tests/test_black.py b/tests/test_black.py index 92598532e2c..2d0a7dfd4e2 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -811,6 +811,24 @@ def test_get_features_used(self) -> None: node = black.lib2to3_parse("def fn(a, /, b): ...") self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) + def test_get_features_used_for_future_flags(self) -> None: + for src, features in [ + ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}), + ( + "from __future__ import (other, annotations)", + {Feature.FUTURE_ANNOTATIONS}, + ), + ("a = 1 + 2\nfrom something import annotations", set()), + ("from __future__ import x, y", set()), + ]: + with self.subTest(src=src, features=features): + node = black.lib2to3_parse(src) + future_imports = black.get_future_imports(node) + self.assertEqual( + black.get_features_used(node, future_imports=future_imports), + features, + ) + def test_get_future_imports(self) -> None: node = black.lib2to3_parse("\n") self.assertEqual(set(), black.get_future_imports(node)) From 3083f4470bba6838d0ad59e9748f45a7621623b5 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:32:14 -0500 Subject: [PATCH 083/700] Don't colour diff headers white, only bold (GH-2691) So people with light themed terminals can still read 'em. --- CHANGES.md | 2 ++ src/black/output.py | 2 +- tests/test_black.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 87e36f4dbe7..c73295c4a0d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ - Improve error message for invalid regular expression (#2678) - Fix mapping cases that contain as-expressions, like `case {"key": 1 | 2 as password}` (#2686) +- No longer color diff headers white as it's unreadable in light themed terminals + (#2691) ## 21.12b0 diff --git a/src/black/output.py b/src/black/output.py index f030d0a0d08..9561d4b57d2 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -81,7 +81,7 @@ def color_diff(contents: str) -> str: lines = contents.split("\n") for i, line in enumerate(lines): if line.startswith("+++") or line.startswith("---"): - line = "\033[1;37m" + line + "\033[0m" # bold white, reset + line = "\033[1m" + line + "\033[0m" # bold, reset elif line.startswith("@@"): line = "\033[36m" + line + "\033[0m" # cyan, reset elif line.startswith("+"): diff --git a/tests/test_black.py b/tests/test_black.py index 2d0a7dfd4e2..468f00fcafb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -200,7 +200,7 @@ def test_piping_diff_with_color(self) -> None: ) actual = result.output # Again, the contents are checked in a different test, so only look for colors. - self.assertIn("\033[1;37m", actual) + self.assertIn("\033[1m", actual) self.assertIn("\033[36m", actual) self.assertIn("\033[32m", actual) self.assertIn("\033[31m", actual) @@ -323,7 +323,7 @@ def test_expression_diff_with_color(self) -> None: actual = result.output # We check the contents of the diff in `test_expression_diff`. All # we need to check here is that color codes exist in the result. - self.assertIn("\033[1;37m", actual) + self.assertIn("\033[1m", actual) self.assertIn("\033[36m", actual) self.assertIn("\033[32m", actual) self.assertIn("\033[31m", actual) From 72fbacd996b7337af6659d1d7c280e401733af4b Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 14 Dec 2021 20:25:47 -0500 Subject: [PATCH 084/700] chore: dump docs deps and pre-commit hooks (#2676) --- .github/dependabot.yml | 17 +++++++++++++++++ .pre-commit-config.yaml | 4 ++-- docs/requirements.txt | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..1d9c3ccc032 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + # Workflow files in .github/workflows will be checked + directory: "/" + schedule: + interval: "weekly" + labels: ["skip news", "C: dependencies"] + + - package-ecosystem: "python" + directory: "docs/" + schedule: + interval: "weekly" + labels: ["skip news", "C: dependencies", "T: documentation"] + reviewers: ["ichard26"] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45810d2844a..52a18623612 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy exclude: ^docs/conf.py @@ -54,7 +54,7 @@ repos: - platformdirs >= 2.1.0 - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.3.2 + rev: v2.5.1 hooks: - id: prettier exclude: ^Pipfile\.lock diff --git a/docs/requirements.txt b/docs/requirements.txt index 296efc5cc84..b15d6b62c39 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.15.1 -Sphinx==4.2.0 +myst-parser==0.15.2 +Sphinx==4.3.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From 93701d249e2cadf0ec096a752a5cbbe8da1a1130 Mon Sep 17 00:00:00 2001 From: aru Date: Tue, 14 Dec 2021 21:21:15 -0500 Subject: [PATCH 085/700] use valid package-ecosystem key (#2694) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1d9c3ccc032..325cb31af1c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: interval: "weekly" labels: ["skip news", "C: dependencies"] - - package-ecosystem: "python" + - package-ecosystem: "pip" directory: "docs/" schedule: interval: "weekly" From 3501cefb09eb8448bd82287840c9093f10c25299 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 14 Dec 2021 21:21:28 -0500 Subject: [PATCH 086/700] Include underlying error when AST safety check parsing fails (#2693) --- CHANGES.md | 2 ++ src/black/__init__.py | 2 +- tests/test_black.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c73295c4a0d..9208be7cd96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ### _Black_ - Improve error message for invalid regular expression (#2678) +- Improve error message when parsing fails during AST safety check by embedding the + underlying SyntaxError (#2693) - Fix mapping cases that contain as-expressions, like `case {"key": 1 | 2 as password}` (#2686) - No longer color diff headers white as it's unreadable in light themed terminals diff --git a/src/black/__init__.py b/src/black/__init__.py index 59018d00de4..f2efdec83b2 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1305,7 +1305,7 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: src_ast = parse_ast(src) except Exception as exc: raise AssertionError( - "cannot use --safe with this file; failed to parse source file." + f"cannot use --safe with this file; failed to parse source file: {exc}" ) from exc try: diff --git a/tests/test_black.py b/tests/test_black.py index 468f00fcafb..63cd716c0bb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1584,6 +1584,16 @@ def test_for_handled_unexpected_eof_error(self) -> None: exc_info.match("Cannot parse: 2:0: EOF in multi-line statement") + def test_equivalency_ast_parse_failure_includes_error(self) -> None: + with pytest.raises(AssertionError) as err: + black.assert_equivalent("a«»a = 1", "a«»a = 1") + + err.match("--safe") + # Unfortunately the SyntaxError message has changed in newer versions so we + # can't match it directly. + err.match("invalid character") + err.match(r"\(, line 1\)") + class TestCaching: def test_cache_broken_file(self) -> None: From e9f520c16abc8a864b61ae658bc3c91fda46fdd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Dec 2021 18:26:01 -0500 Subject: [PATCH 087/700] Bump myst-parser from 0.15.2 to 0.16.0 in /docs (GH-2696) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 0.15.2 to 0.16.0. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v0.15.2...v0.16.0) --- updated-dependencies: - dependency-name: myst-parser 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 b15d6b62c39..57eccb7818f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.15.2 +myst-parser==0.16.0 Sphinx==4.3.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From f10ce0c942b41cd4c6802ba690a432c6adedc05e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Dec 2021 18:55:26 -0500 Subject: [PATCH 088/700] Bump pre-commit/action from 2.0.2 to 2.0.3 (GH-2695) Bumps [pre-commit/action](https://github.com/pre-commit/action) from 2.0.2 to 2.0.3. - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](https://github.com/pre-commit/action/compare/v2.0.2...v2.0.3) --- updated-dependencies: - dependency-name: pre-commit/action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 51f6d02e2e6..2f6c504d3f2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,4 +25,4 @@ jobs: python -m pip install -e '.[d]' - name: Lint - uses: pre-commit/action@v2.0.2 + uses: pre-commit/action@v2.0.3 From dc90d4951f66ac665582159537b902017d9a0361 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 16 Dec 2021 03:17:33 +0300 Subject: [PATCH 089/700] Unpacking on flow constructs (return/yield) now implies 3.8+ (#2700) --- CHANGES.md | 1 + src/black/__init__.py | 8 ++++++++ src/black/mode.py | 4 ++++ tests/test_black.py | 8 ++++++++ 4 files changed, 21 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 9208be7cd96..ae0bf80da48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ (#2686) - 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) ## 21.12b0 diff --git a/src/black/__init__.py b/src/black/__init__.py index f2efdec83b2..08c239dc155 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1210,6 +1210,14 @@ def get_features_used( # noqa: C901 if argch.type in STARS: features.add(feature) + elif ( + n.type in {syms.return_stmt, syms.yield_expr} + and len(n.children) >= 2 + and n.children[1].type == syms.testlist_star_expr + and any(child.type == syms.star_expr for child in n.children[1].children) + ): + features.add(Feature.UNPACKING_ON_FLOW) + # Python 2 only features (for its deprecation) except for integers, see above elif n.type == syms.print_stmt: features.add(Feature.PRINT_STMT) diff --git a/src/black/mode.py b/src/black/mode.py index a2b7d9e9e2d..b28dcd8d149 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -49,6 +49,7 @@ class Feature(Enum): POS_ONLY_ARGUMENTS = 9 RELAXED_DECORATORS = 10 PATTERN_MATCHING = 11 + UNPACKING_ON_FLOW = 12 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -116,6 +117,7 @@ class Feature(Enum): Feature.FUTURE_ANNOTATIONS, Feature.ASSIGNMENT_EXPRESSIONS, Feature.POS_ONLY_ARGUMENTS, + Feature.UNPACKING_ON_FLOW, }, TargetVersion.PY39: { Feature.UNICODE_LITERALS, @@ -128,6 +130,7 @@ class Feature(Enum): Feature.ASSIGNMENT_EXPRESSIONS, Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, + Feature.UNPACKING_ON_FLOW, }, TargetVersion.PY310: { Feature.UNICODE_LITERALS, @@ -140,6 +143,7 @@ class Feature(Enum): Feature.ASSIGNMENT_EXPRESSIONS, Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, + Feature.UNPACKING_ON_FLOW, Feature.PATTERN_MATCHING, }, } diff --git a/tests/test_black.py b/tests/test_black.py index 63cd716c0bb..8726cc10ddc 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -810,6 +810,14 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) node = black.lib2to3_parse("def fn(a, /, b): ...") self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) + node = black.lib2to3_parse("def fn(): yield a, b") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("def fn(): return a, b") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("def fn(): yield *b, c") + self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) + node = black.lib2to3_parse("def fn(): return a, *b, c") + self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From 61fe8418cc868723759fb08d76adab1542bb7630 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 16 Dec 2021 16:35:01 +1300 Subject: [PATCH 090/700] Use 'python -m build' to build wheel and source distributions (#2701) --- .github/workflows/pypi_upload.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 201d94fd85e..0921b624c45 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -15,14 +15,14 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 - - name: Install latest pip, setuptools, twine + wheel + - name: Install latest pip, build, twine run: | - python -m pip install --upgrade pip setuptools twine wheel + python -m pip install --upgrade --disable-pip-version-check pip + python -m pip install --upgrade build twine - - name: Build wheels + - name: Build wheel and source distributions run: | - python setup.py bdist_wheel - python setup.py sdist + python -m build - name: Upload to PyPI via Twine env: From b97ec62368ac57b29a0ccd5fc68ba875418eb8cc Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sat, 18 Dec 2021 00:43:14 +0300 Subject: [PATCH 091/700] Imply 3.8+ when annotated assigments used with unparenthesized tuples (#2708) --- CHANGES.md | 2 ++ src/black/__init__.py | 7 +++++++ src/black/mode.py | 4 ++++ tests/test_black.py | 12 ++++++++++++ 4 files changed, 25 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index ae0bf80da48..0452f1820c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ - 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) ## 21.12b0 diff --git a/src/black/__init__.py b/src/black/__init__.py index 08c239dc155..d8b98196aa0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1218,6 +1218,13 @@ def get_features_used( # noqa: C901 ): features.add(Feature.UNPACKING_ON_FLOW) + elif ( + n.type == syms.annassign + and len(n.children) >= 4 + and n.children[3].type == syms.testlist_star_expr + ): + features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) + # Python 2 only features (for its deprecation) except for integers, see above elif n.type == syms.print_stmt: features.add(Feature.PRINT_STMT) diff --git a/src/black/mode.py b/src/black/mode.py index b28dcd8d149..bd4428add66 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -50,6 +50,7 @@ class Feature(Enum): RELAXED_DECORATORS = 10 PATTERN_MATCHING = 11 UNPACKING_ON_FLOW = 12 + ANN_ASSIGN_EXTENDED_RHS = 13 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -118,6 +119,7 @@ class Feature(Enum): Feature.ASSIGNMENT_EXPRESSIONS, Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, + Feature.ANN_ASSIGN_EXTENDED_RHS, }, TargetVersion.PY39: { Feature.UNICODE_LITERALS, @@ -131,6 +133,7 @@ class Feature(Enum): Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, + Feature.ANN_ASSIGN_EXTENDED_RHS, }, TargetVersion.PY310: { Feature.UNICODE_LITERALS, @@ -144,6 +147,7 @@ class Feature(Enum): Feature.RELAXED_DECORATORS, Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, + Feature.ANN_ASSIGN_EXTENDED_RHS, Feature.PATTERN_MATCHING, }, } diff --git a/tests/test_black.py b/tests/test_black.py index 8726cc10ddc..628647ed977 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -818,6 +818,18 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) node = black.lib2to3_parse("def fn(): return a, *b, c") self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) + node = black.lib2to3_parse("x = a, *b, c") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("x: Any = regular") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("x: Any = (regular, regular)") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c") + self.assertEqual( + black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} + ) def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From 6ef3e466db7ad91dd6300d2412222ec912ae56e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 07:24:53 -0800 Subject: [PATCH 092/700] Bump sphinx from 4.3.1 to 4.3.2 in /docs (#2709) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.1 to 4.3.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.1...v4.3.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... 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 57eccb7818f..f08c389f459 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.16.0 -Sphinx==4.3.1 +Sphinx==4.3.2 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From c5b458ef4ba1b6a62685461c6731f1775bee132b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 07:42:03 -0800 Subject: [PATCH 093/700] Bump myst-parser from 0.16.0 to 0.16.1 in /docs (#2710) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 0.16.0 to 0.16.1. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v0.16.0...v0.16.1) --- updated-dependencies: - dependency-name: myst-parser dependency-type: direct:production update-type: version-update:semver-patch ... 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 f08c389f459..0cdef2a39dd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.16.0 +myst-parser==0.16.1 Sphinx==4.3.2 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From 389e9c23a9e622ee6090d902cc5f56c5f76cdee9 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:03:07 +0200 Subject: [PATCH 094/700] Disable universal newlines when reading TOML (#2408) --- CHANGES.md | 1 + Pipfile | 2 +- setup.py | 2 +- src/black/files.py | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0452f1820c4..252f2cc8863 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- 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) diff --git a/Pipfile b/Pipfile index a3af5fd8844..90d4a2a78c1 100644 --- a/Pipfile +++ b/Pipfile @@ -42,7 +42,7 @@ platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" -tomli = ">=0.2.6, <2.0.0" +tomli = ">=1.1.0, <3.0.0" typed-ast = "==1.4.3" typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} black = {editable = true,extras = ["d"],path = "."} diff --git a/setup.py b/setup.py index a21bc87264d..d314bb283f2 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def find_python_files(base: Path) -> List[Path]: install_requires=[ "click>=7.1.2", "platformdirs>=2", - "tomli>=0.2.6,<2.0.0", + "tomli>=1.1.0,<3.0.0", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", diff --git a/src/black/files.py b/src/black/files.py index 560aa05080d..dfab9f73039 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -95,8 +95,8 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: If parsing fails, will raise a tomli.TOMLDecodeError """ - with open(path_config, encoding="utf8") as f: - pyproject_toml = tomli.loads(f.read()) + with open(path_config, "rb") as f: + pyproject_toml = tomli.load(f) config = pyproject_toml.get("tool", {}).get("black", {}) return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} From 7c94ed61a55f8ae0c60737cbc6cfee3b5066ce11 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Tue, 21 Dec 2021 16:20:55 +0000 Subject: [PATCH 095/700] Define is_name_token (and friends) to resolve some `type: ignore`s (GH-2714) Gets rid of a few # type: ignores by using TypeGuard. --- src/black/__init__.py | 5 +++-- src/black/linegen.py | 13 +++++++------ src/black/nodes.py | 28 ++++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index d8b98196aa0..9bc8fc15c49 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -37,6 +37,7 @@ from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES from black.const import STDIN_PLACEHOLDER from black.nodes import STARS, syms, is_simple_decorator_expression +from black.nodes import is_string_token from black.lines import Line, EmptyLineTracker from black.linegen import transform_line, LineGenerator, LN from black.comments import normalize_fmt_off @@ -1156,8 +1157,8 @@ def get_features_used( # noqa: C901 } for n in node.pre_order(): - if n.type == token.STRING: - value_head = n.value[:2] # type: ignore + if is_string_token(n): + value_head = n.value[:2] if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}: features.add(Feature.F_STRINGS) diff --git a/src/black/linegen.py b/src/black/linegen.py index f234913a161..c1cd6fa22d9 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -9,6 +9,7 @@ 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 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 from black.nodes import is_stub_suite, is_stub_body, is_atom_with_invisible_parens from black.nodes import wrap_in_parentheses @@ -137,7 +138,7 @@ def visit_stmt( """ normalize_invisible_parens(node, parens_after=parens) for child in node.children: - if child.type == token.NAME and child.value in keywords: # type: ignore + if is_name_token(child) and child.value in keywords: yield from self.line() yield from self.visit(child) @@ -813,9 +814,9 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: elif node.type == syms.import_from: # "import from" nodes store parentheses directly as part of # the statement - if child.type == token.LPAR: + if is_lpar_token(child): # make parentheses invisible - child.value = "" # type: ignore + child.value = "" node.children[-1].value = "" # type: ignore elif child.type != token.STAR: # insert invisible parentheses @@ -861,11 +862,11 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: first = node.children[0] last = node.children[-1] - if first.type == token.LPAR and last.type == token.RPAR: + if is_lpar_token(first) and is_rpar_token(last): middle = node.children[1] # make parentheses invisible - first.value = "" # type: ignore - last.value = "" # type: ignore + first.value = "" + last.value = "" maybe_make_parens_invisible_in_atom(middle, parent=parent) if is_atom_with_invisible_parens(middle): diff --git a/src/black/nodes.py b/src/black/nodes.py index 8bf1934bc2a..75a23474024 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -19,11 +19,15 @@ from typing import Final else: from typing_extensions import Final +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard from mypy_extensions import mypyc_attr # lib2to3 fork -from blib2to3.pytree import Node, Leaf, type_repr +from blib2to3.pytree import Node, Leaf, type_repr, NL from blib2to3 import pygram from blib2to3.pgen2 import token @@ -260,8 +264,8 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 and prevp.parent and prevp.parent.type == syms.shift_expr and prevp.prev_sibling - and prevp.prev_sibling.type == token.NAME - and prevp.prev_sibling.value == "print" # type: ignore + and is_name_token(prevp.prev_sibling) + and prevp.prev_sibling.value == "print" ): # Python 2 print chevron return NO @@ -687,7 +691,7 @@ def is_yield(node: LN) -> bool: if node.type == syms.yield_expr: return True - if node.type == token.NAME and node.value == "yield": # type: ignore + if is_name_token(node) and node.value == "yield": return True if node.type != syms.atom: @@ -854,3 +858,19 @@ def ensure_visible(leaf: Leaf) -> None: leaf.value = "(" elif leaf.type == token.RPAR: leaf.value = ")" + + +def is_name_token(nl: NL) -> TypeGuard[Leaf]: + return nl.type == token.NAME + + +def is_lpar_token(nl: NL) -> TypeGuard[Leaf]: + return nl.type == token.LPAR + + +def is_rpar_token(nl: NL) -> TypeGuard[Leaf]: + return nl.type == token.RPAR + + +def is_string_token(nl: NL) -> TypeGuard[Leaf]: + return nl.type == token.STRING From c758126a270bb5a78513f3f07ddd60bc4aacf4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 21 Dec 2021 17:24:20 +0100 Subject: [PATCH 096/700] Remove usage of Pipenv, rely on good ol' `pip` and `virtualenv` in docs (#2717) --- Pipfile | 49 - Pipfile.lock | 1958 ------------------------------- docs/contributing/the_basics.md | 33 +- 3 files changed, 12 insertions(+), 2028 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 90d4a2a78c1..00000000000 --- a/Pipfile +++ /dev/null @@ -1,49 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.python.org/simple" -verify_ssl = true - -[dev-packages] -# Testing related requirements. -coverage = ">= 5.3" -pytest = " >= 6.1.1" -pytest-xdist = ">= 2.2.1" -pytest-cov = ">= 2.11.1" -tox = "*" - -# Linting related requirements. -pre-commit = ">=2.9.2" -flake8 = ">=3.9.2" -flake8-bugbear = "*" -mypy = ">=0.910" -types-dataclasses = ">=0.1.3" -types-typed-ast = ">=1.4.1" -types-PyYAML = ">=5.4.1" - -# Documentation related requirements. -Sphinx = ">=4.1.2" -MyST-Parser = ">=0.15.1" -sphinxcontrib-programoutput = ">=0.17" -sphinx-copybutton = ">=0.4.0" -docutils = "==0.17.1" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 - -# Packaging related requirements. -setuptools = ">=39.2.0" -setuptools-scm = "*" -twine = ">=1.11.0" -wheel = ">=0.31.1" -readme_renderer = "*" - -black = {editable = true, extras = ["d", "jupyter"], path = "."} - -[packages] -aiohttp = ">=3.7.4" -platformdirs= ">=2" -click = ">=8.0.0" -mypy_extensions = ">=0.4.3" -pathspec = ">=0.8.1" -tomli = ">=1.1.0, <3.0.0" -typed-ast = "==1.4.3" -typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"} -black = {editable = true,extras = ["d"],path = "."} -dataclasses = {markers = "python_version < '3.7'", version = ">0.1.3"} diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index b2a9f6c6fc0..00000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1958 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "7728caac52b47ed119a804ead88afa002d62c17a324e962b7833b8944049609b" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiohttp": { - "hashes": [ - "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984", - "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3", - "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8", - "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4", - "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33", - "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b", - "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e", - "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc", - "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7", - "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c", - "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8", - "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934", - "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079", - "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919", - "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f", - "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795", - "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6", - "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c", - "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e", - "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769", - "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b", - "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671", - "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0", - "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c", - "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4", - "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94", - "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e", - "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0", - "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3", - "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2", - "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41", - "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990", - "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847", - "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a", - "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993", - "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385", - "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3", - "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a", - "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19", - "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739", - "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4", - "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4", - "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9", - "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769", - "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4", - "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3", - "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab", - "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5", - "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe", - "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8", - "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9", - "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e", - "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd", - "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5", - "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45", - "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195", - "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc", - "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150", - "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e", - "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d", - "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661", - "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3", - "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e", - "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c", - "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f", - "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321", - "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f", - "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d", - "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba", - "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19", - "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722", - "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79" - ], - "index": "pypi", - "version": "==3.8.0" - }, - "aiosignal": { - "hashes": [ - "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", - "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" - }, - "async-timeout": { - "hashes": [ - "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690", - "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "asynctest": { - "hashes": [ - "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676", - "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac" - ], - "markers": "python_version < '3.8'", - "version": "==0.13.0" - }, - "attrs": { - "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "black": { - "editable": true, - "extras": [ - "d" - ], - "path": "." - }, - "charset-normalizer": { - "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.7" - }, - "click": { - "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" - ], - "index": "pypi", - "version": "==8.0.3" - }, - "dataclasses": { - "hashes": [ - "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", - "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" - ], - "index": "pypi", - "markers": "python_version < '3.7'", - "version": "==0.8" - }, - "frozenlist": { - "hashes": [ - "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c", - "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9", - "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00", - "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161", - "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193", - "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c", - "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d", - "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315", - "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f", - "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9", - "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4", - "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a", - "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020", - "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b", - "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d", - "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a", - "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f", - "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4", - "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837", - "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3", - "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257", - "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38", - "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03", - "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc", - "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43", - "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d", - "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b", - "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f", - "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210", - "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3", - "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de", - "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6", - "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a", - "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b", - "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee", - "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19", - "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15", - "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3", - "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59", - "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b", - "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73", - "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca", - "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4", - "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae", - "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034", - "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9", - "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2", - "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676", - "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618", - "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4", - "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc", - "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397", - "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a", - "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd", - "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729", - "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408", - "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d", - "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0", - "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53", - "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f", - "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2", - "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b", - "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d", - "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792", - "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697", - "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2", - "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb", - "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d", - "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0", - "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367", - "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673", - "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "idna-ssl": { - "hashes": [ - "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" - ], - "markers": "python_version < '3.7'", - "version": "==1.1.0" - }, - "importlib-metadata": { - "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" - ], - "markers": "python_version < '3.8'", - "version": "==4.8.2" - }, - "multidict": { - "hashes": [ - "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", - "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", - "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", - "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", - "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", - "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", - "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", - "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", - "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", - "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", - "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", - "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", - "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", - "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", - "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", - "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", - "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", - "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", - "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", - "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", - "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", - "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", - "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", - "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", - "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", - "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", - "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", - "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", - "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", - "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", - "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", - "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", - "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", - "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", - "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", - "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", - "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", - "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", - "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", - "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", - "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", - "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", - "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", - "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", - "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", - "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", - "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", - "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", - "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", - "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", - "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", - "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", - "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", - "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", - "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", - "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", - "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", - "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", - "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", - "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", - "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", - "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", - "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", - "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", - "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", - "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", - "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", - "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", - "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", - "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", - "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", - "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" - ], - "markers": "python_version >= '3.6'", - "version": "==5.2.0" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "index": "pypi", - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", - "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" - ], - "markers": "python_version >= '3.6'", - "version": "==21.2" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "index": "pypi", - "version": "==0.9.0" - }, - "platformdirs": { - "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" - ], - "index": "pypi", - "version": "==2.4.0" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" - }, - "setuptools": { - "hashes": [ - "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf", - "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729" - ], - "markers": "python_version >= '3.6'", - "version": "==58.5.3" - }, - "setuptools-scm": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119", - "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2" - ], - "markers": "python_version >= '3.6'", - "version": "==6.3.2" - }, - "tomli": { - "hashes": [ - "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", - "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" - ], - "index": "pypi", - "version": "==1.2.2" - }, - "typed-ast": { - "hashes": [ - "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", - "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", - "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", - "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", - "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", - "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", - "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", - "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", - "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", - "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", - "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", - "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", - "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", - "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", - "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", - "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", - "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", - "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", - "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", - "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", - "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", - "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", - "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", - "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", - "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", - "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", - "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", - "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", - "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", - "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" - ], - "index": "pypi", - "version": "==1.4.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", - "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", - "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" - ], - "index": "pypi", - "markers": "python_version < '3.10'", - "version": "==3.10.0.2" - }, - "yarl": { - "hashes": [ - "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", - "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", - "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", - "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", - "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", - "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", - "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", - "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", - "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", - "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", - "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", - "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", - "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", - "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", - "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", - "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", - "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", - "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", - "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", - "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", - "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", - "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", - "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", - "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", - "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", - "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", - "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", - "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", - "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", - "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", - "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", - "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", - "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", - "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", - "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", - "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", - "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", - "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", - "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", - "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", - "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", - "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", - "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", - "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", - "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", - "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", - "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", - "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", - "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", - "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", - "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", - "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", - "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", - "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", - "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", - "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", - "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", - "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", - "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", - "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", - "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", - "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", - "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", - "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", - "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", - "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", - "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", - "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", - "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", - "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", - "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", - "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" - ], - "markers": "python_version >= '3.6'", - "version": "==1.7.2" - }, - "zipp": { - "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" - ], - "markers": "python_version >= '3.6'", - "version": "==3.6.0" - } - }, - "develop": { - "aiohttp": { - "hashes": [ - "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984", - "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3", - "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8", - "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4", - "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33", - "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b", - "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e", - "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc", - "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7", - "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c", - "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8", - "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934", - "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079", - "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919", - "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f", - "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795", - "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6", - "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c", - "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e", - "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769", - "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b", - "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671", - "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0", - "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c", - "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4", - "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94", - "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e", - "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0", - "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3", - "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2", - "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41", - "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990", - "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847", - "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a", - "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993", - "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385", - "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3", - "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a", - "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19", - "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739", - "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4", - "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4", - "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9", - "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769", - "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4", - "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3", - "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab", - "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5", - "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe", - "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8", - "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9", - "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e", - "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd", - "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5", - "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45", - "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195", - "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc", - "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150", - "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e", - "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d", - "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661", - "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3", - "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e", - "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c", - "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f", - "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321", - "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f", - "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d", - "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba", - "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19", - "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722", - "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79" - ], - "index": "pypi", - "version": "==3.8.0" - }, - "aiosignal": { - "hashes": [ - "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", - "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" - }, - "alabaster": { - "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" - ], - "version": "==0.7.12" - }, - "appnope": { - "hashes": [ - "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", - "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.2" - }, - "async-timeout": { - "hashes": [ - "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690", - "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "asynctest": { - "hashes": [ - "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676", - "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac" - ], - "markers": "python_version < '3.8'", - "version": "==0.13.0" - }, - "attrs": { - "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "babel": { - "hashes": [ - "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", - "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.1" - }, - "backcall": { - "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" - ], - "version": "==0.2.0" - }, - "backports.entry-points-selectable": { - "hashes": [ - "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b", - "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386" - ], - "markers": "python_version >= '2.7'", - "version": "==1.1.1" - }, - "black": { - "editable": true, - "extras": [ - "d" - ], - "path": "." - }, - "bleach": { - "hashes": [ - "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", - "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "cfgv": { - "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.7" - }, - "click": { - "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" - ], - "index": "pypi", - "version": "==8.0.3" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" - }, - "coverage": { - "hashes": [ - "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954", - "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0", - "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193", - "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052", - "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e", - "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c", - "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d", - "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4", - "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186", - "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d", - "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696", - "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13", - "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2", - "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c", - "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388", - "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9", - "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59", - "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225", - "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e", - "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b", - "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a", - "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f", - "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93", - "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758", - "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b", - "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b", - "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204", - "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71", - "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd", - "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373", - "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4", - "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc", - "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266", - "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263", - "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf", - "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c", - "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c", - "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc", - "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c", - "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649", - "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972", - "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f", - "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929", - "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d", - "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de", - "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091", - "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab" - ], - "index": "pypi", - "version": "==6.1.2" - }, - "cryptography": { - "hashes": [ - "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6", - "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6", - "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c", - "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999", - "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e", - "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992", - "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d", - "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588", - "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa", - "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d", - "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd", - "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d", - "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953", - "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2", - "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8", - "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6", - "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9", - "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6", - "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad", - "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76" - ], - "markers": "python_version >= '3.6'", - "version": "==35.0.0" - }, - "dataclasses": { - "hashes": [ - "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", - "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" - ], - "index": "pypi", - "markers": "python_version < '3.7'", - "version": "==0.8" - }, - "decorator": { - "hashes": [ - "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374", - "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" - ], - "markers": "python_version >= '3.5'", - "version": "==5.1.0" - }, - "distlib": { - "hashes": [ - "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31", - "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05" - ], - "version": "==0.3.3" - }, - "docutils": { - "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" - ], - "index": "pypi", - "version": "==0.17.1" - }, - "execnet": { - "hashes": [ - "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", - "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.9.0" - }, - "filelock": { - "hashes": [ - "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8", - "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b" - ], - "markers": "python_version >= '3.6'", - "version": "==3.3.2" - }, - "flake8": { - "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" - ], - "index": "pypi", - "version": "==4.0.1" - }, - "flake8-bugbear": { - "hashes": [ - "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769", - "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2" - ], - "index": "pypi", - "version": "==21.9.2" - }, - "frozenlist": { - "hashes": [ - "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c", - "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9", - "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00", - "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161", - "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193", - "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c", - "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d", - "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315", - "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f", - "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9", - "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4", - "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a", - "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020", - "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b", - "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d", - "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a", - "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f", - "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4", - "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837", - "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3", - "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257", - "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38", - "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03", - "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc", - "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43", - "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d", - "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b", - "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f", - "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210", - "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3", - "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de", - "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6", - "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a", - "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b", - "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee", - "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19", - "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15", - "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3", - "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59", - "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b", - "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73", - "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca", - "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4", - "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae", - "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034", - "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9", - "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2", - "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676", - "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618", - "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4", - "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc", - "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397", - "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a", - "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd", - "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729", - "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408", - "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d", - "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0", - "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53", - "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f", - "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2", - "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b", - "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d", - "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792", - "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697", - "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2", - "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb", - "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d", - "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0", - "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367", - "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673", - "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" - }, - "identify": { - "hashes": [ - "sha256:6f0368ba0f21c199645a331beb7425d5374376e71bc149e9cb55e45cb45f832d", - "sha256:ba945bddb4322394afcf3f703fa68eda08a6acc0f99d9573eb2be940aa7b9bba" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.3.5" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "idna-ssl": { - "hashes": [ - "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" - ], - "markers": "python_version < '3.7'", - "version": "==1.1.0" - }, - "imagesize": { - "hashes": [ - "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", - "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.0" - }, - "importlib-metadata": { - "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" - ], - "markers": "python_version < '3.8'", - "version": "==4.8.2" - }, - "importlib-resources": { - "hashes": [ - "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45", - "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b" - ], - "markers": "python_version < '3.7'", - "version": "==5.4.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "ipython": { - "hashes": [ - "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64", - "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf" - ], - "markers": "python_version >= '3.6'", - "version": "==7.16.1" - }, - "ipython-genutils": { - "hashes": [ - "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", - "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" - ], - "version": "==0.2.0" - }, - "jedi": { - "hashes": [ - "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", - "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.0" - }, - "jeepney": { - "hashes": [ - "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", - "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.7.1" - }, - "jinja2": { - "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" - }, - "keyring": { - "hashes": [ - "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe", - "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e" - ], - "markers": "python_version >= '3.6'", - "version": "==23.2.1" - }, - "markdown-it-py": { - "hashes": [ - "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3", - "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389" - ], - "markers": "python_version ~= '3.6'", - "version": "==1.1.0" - }, - "markupsafe": { - "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" - }, - "matplotlib-inline": { - "hashes": [ - "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", - "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.3" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "mdit-py-plugins": { - "hashes": [ - "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c", - "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f" - ], - "markers": "python_version ~= '3.6'", - "version": "==0.2.8" - }, - "multidict": { - "hashes": [ - "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", - "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", - "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", - "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", - "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", - "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", - "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", - "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", - "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", - "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", - "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", - "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", - "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", - "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", - "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", - "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", - "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", - "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", - "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", - "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", - "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", - "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", - "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", - "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", - "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", - "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", - "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", - "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", - "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", - "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", - "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", - "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", - "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", - "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", - "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", - "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", - "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", - "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", - "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", - "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", - "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", - "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", - "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", - "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", - "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", - "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", - "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", - "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", - "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", - "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", - "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", - "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", - "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", - "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", - "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", - "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", - "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", - "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", - "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", - "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", - "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", - "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", - "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", - "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", - "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", - "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", - "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", - "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", - "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", - "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", - "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", - "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" - ], - "markers": "python_version >= '3.6'", - "version": "==5.2.0" - }, - "mypy": { - "hashes": [ - "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", - "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", - "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", - "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", - "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", - "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", - "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", - "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", - "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", - "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", - "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", - "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", - "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", - "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", - "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", - "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", - "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", - "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", - "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", - "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", - "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", - "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", - "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" - ], - "index": "pypi", - "version": "==0.910" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "index": "pypi", - "version": "==0.4.3" - }, - "myst-parser": { - "hashes": [ - "sha256:40124b6f27a4c42ac7f06b385e23a9dcd03d84801e9c7130b59b3729a554b1f9", - "sha256:f7f3b2d62db7655cde658eb5d62b2ec2a4631308137bd8d10f296a40d57bbbeb" - ], - "index": "pypi", - "version": "==0.15.2" - }, - "nodeenv": { - "hashes": [ - "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", - "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" - ], - "version": "==1.6.0" - }, - "packaging": { - "hashes": [ - "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", - "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" - ], - "markers": "python_version >= '3.6'", - "version": "==21.2" - }, - "parso": { - "hashes": [ - "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", - "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.2" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "index": "pypi", - "version": "==0.9.0" - }, - "pexpect": { - "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.8.0" - }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" - }, - "pkginfo": { - "hashes": [ - "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779", - "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd" - ], - "version": "==1.7.1" - }, - "platformdirs": { - "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" - ], - "index": "pypi", - "version": "==2.4.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "pre-commit": { - "hashes": [ - "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7", - "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6" - ], - "index": "pypi", - "version": "==2.15.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:449f333dd120bd01f5d296a8ce1452114ba3a71fae7288d2f0ae2c918764fa72", - "sha256:48d85cdca8b6c4f16480c7ce03fd193666b62b0a21667ca56b4bb5ad679d1170" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.22" - }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pyflakes": { - "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" - }, - "pytest": { - "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" - ], - "index": "pypi", - "version": "==6.2.5" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-forked": { - "hashes": [ - "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca", - "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.3.0" - }, - "pytest-xdist": { - "hashes": [ - "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168", - "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9" - ], - "index": "pypi", - "version": "==2.4.0" - }, - "pytz": { - "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" - ], - "version": "==2021.3" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "readme-renderer": { - "hashes": [ - "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc", - "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8" - ], - "index": "pypi", - "version": "==30.0" - }, - "requests": { - "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.26.0" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" - ], - "version": "==0.9.1" - }, - "rfc3986": { - "hashes": [ - "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", - "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" - ], - "version": "==1.5.0" - }, - "secretstorage": { - "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.1" - }, - "setuptools": { - "hashes": [ - "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf", - "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729" - ], - "markers": "python_version >= '3.6'", - "version": "==58.5.3" - }, - "setuptools-scm": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119", - "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2" - ], - "markers": "python_version >= '3.6'", - "version": "==6.3.2" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "snowballstemmer": { - "hashes": [ - "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", - "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" - ], - "version": "==2.1.0" - }, - "sphinx": { - "hashes": [ - "sha256:6d051ab6e0d06cba786c4656b0fe67ba259fe058410f49e95bee6e49c4052cbf", - "sha256:7e2b30da5f39170efcd95c6270f07669d623c276521fee27ad6c380f49d2bf5b" - ], - "index": "pypi", - "version": "==4.3.0" - }, - "sphinx-copybutton": { - "hashes": [ - "sha256:4340d33c169dac6dd82dce2c83333412aa786a42dd01a81a8decac3b130dc8b0", - "sha256:8daed13a87afd5013c3a9af3575cc4d5bec052075ccd3db243f895c07a689386" - ], - "index": "pypi", - "version": "==0.4.0" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", - "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", - "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", - "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-programoutput": { - "hashes": [ - "sha256:0ef1c1d9159dbe7103077748214305eb4e0138e861feb71c0c346afc5fe97f84", - "sha256:300ee9b8caee8355d25cc74b4d1c7efd12e608d2ad165e3141d31e6fbc152b7f" - ], - "index": "pypi", - "version": "==0.17" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", - "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", - "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" - ], - "markers": "python_version >= '3.5'", - "version": "==1.1.5" - }, - "tokenize-rt": { - "hashes": [ - "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8", - "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.2.1" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", - "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" - ], - "index": "pypi", - "version": "==1.2.2" - }, - "tox": { - "hashes": [ - "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10", - "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca" - ], - "index": "pypi", - "version": "==3.24.4" - }, - "tqdm": { - "hashes": [ - "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", - "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.3" - }, - "traitlets": { - "hashes": [ - "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", - "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" - ], - "version": "==4.3.3" - }, - "twine": { - "hashes": [ - "sha256:4caad5ef4722e127b3749052fcbffaaf71719b19d4fd4973b29c469957adeba2", - "sha256:916070f8ecbd1985ebed5dbb02b9bda9a092882a96d7069d542d4fc0bb5c673c" - ], - "index": "pypi", - "version": "==3.6.0" - }, - "typed-ast": { - "hashes": [ - "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", - "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", - "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", - "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", - "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", - "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", - "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", - "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", - "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", - "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", - "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", - "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", - "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", - "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", - "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", - "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", - "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", - "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", - "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", - "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", - "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", - "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", - "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", - "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", - "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", - "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", - "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", - "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", - "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", - "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" - ], - "index": "pypi", - "version": "==1.4.3" - }, - "types-dataclasses": { - "hashes": [ - "sha256:6568532fed11f854e4db2eb48063385b323b93ecadd09f10a215d56246c306d7", - "sha256:aa45bb0dacdba09e3195a36ff8337bba45eac03b6f31c4645e87b4a2a47830dd" - ], - "index": "pypi", - "version": "==0.6.1" - }, - "types-pyyaml": { - "hashes": [ - "sha256:2e27b0118ca4248a646101c5c318dc02e4ca2866d6bc42e84045dbb851555a76", - "sha256:d5b318269652e809b5c30a5fe666c50159ab80bfd41cd6bafe655bf20b29fcba" - ], - "index": "pypi", - "version": "==6.0.1" - }, - "types-typed-ast": { - "hashes": [ - "sha256:4a261b6af545af41fd08957993292742959ca5c480ee8d49804dcc68d78773a3", - "sha256:d8ea79cbfbc520be8d9bc8de4872f44b342dbdbc091667e2f21b03bbd7969150" - ], - "index": "pypi", - "version": "==1.5.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", - "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", - "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" - ], - "index": "pypi", - "markers": "python_version < '3.10'", - "version": "==3.10.0.2" - }, - "urllib3": { - "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" - }, - "virtualenv": { - "hashes": [ - "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814", - "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.10.0" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "wheel": { - "hashes": [ - "sha256:21014b2bd93c6d0034b6ba5d35e4eb284340e09d63c59aef6fc14b0f346146fd", - "sha256:e2ef7239991699e3355d54f8e968a21bb940a1dbf34a4d226741e64462516fad" - ], - "index": "pypi", - "version": "==0.37.0" - }, - "yarl": { - "hashes": [ - "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", - "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", - "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", - "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", - "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", - "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", - "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", - "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", - "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", - "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", - "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", - "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", - "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", - "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", - "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", - "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", - "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", - "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", - "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", - "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", - "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", - "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", - "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", - "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", - "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", - "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", - "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", - "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", - "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", - "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", - "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", - "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", - "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", - "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", - "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", - "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", - "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", - "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", - "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", - "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", - "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", - "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", - "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", - "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", - "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", - "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", - "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", - "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", - "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", - "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", - "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", - "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", - "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", - "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", - "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", - "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", - "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", - "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", - "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", - "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", - "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", - "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", - "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", - "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", - "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", - "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", - "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", - "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", - "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", - "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", - "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", - "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" - ], - "markers": "python_version >= '3.6'", - "version": "==1.7.2" - }, - "zipp": { - "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" - ], - "markers": "python_version >= '3.6'", - "version": "==3.6.0" - } - } -} diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index d61f3ec45b6..d9df0ea30c4 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -10,20 +10,11 @@ You can use any operating system. Install all development dependencies using: ```console -$ pipenv install --dev -$ pipenv shell -$ pre-commit install -``` - -If you haven't used `pipenv` before but are comfortable with virtualenvs, just run -`pip install pipenv` in the virtualenv you're already using and invoke the command above -from the cloned _Black_ repo. It will do the correct thing. - -Non pipenv install works too: - -```console -$ pip install -r test_requirements.txt -$ pip install -e .[d] +$ python3 -m venv .venv +$ source .venv/bin/activate +(.venv)$ pip install -r test_requirements.txt +(.venv)$ pip install -e .[d] +(.venv)$ pre-commit install ``` Before submitting pull requests, run lints and tests with the following commands from @@ -31,16 +22,16 @@ the root of the black repo: ```console # Linting -$ pre-commit run -a +(.venv)$ pre-commit run -a # Unit tests -$ tox -e py +(.venv)$ tox -e py # Optional Fuzz testing -$ tox -e fuzz +(.venv)$ tox -e fuzz # Optional CI run to test your changes on many popular python projects -$ black-primer [-k -w /tmp/black_test_repos] +(.venv)$ black-primer [-k -w /tmp/black_test_repos] ``` ### News / Changelog Requirement @@ -70,9 +61,9 @@ formatting don't need to be mentioned separately though. If you make changes to docs, you can test they still build locally too. ```console -$ pip install -r docs/requirements.txt -$ pip install [-e] .[d] -$ sphinx-build -a -b html -W docs/ docs/_build/ +(.venv)$ pip install -r docs/requirements.txt +(.venv)$ pip install [-e] .[d] +(.venv)$ sphinx-build -a -b html -W docs/ docs/_build/ ``` ## black-primer From 3fafd806b30cbff5788525f050a635639d97b11c Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 21 Dec 2021 21:16:55 +0300 Subject: [PATCH 097/700] Support multiple top-level as-expressions on case statements (#2716) --- CHANGES.md | 2 ++ src/blib2to3/Grammar.txt | 4 ++-- tests/data/pattern_matching_extras.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 252f2cc8863..d5cfb623c9a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ underlying SyntaxError (#2693) - 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) - 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) diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 600712ce2f0..27776a3b65c 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -247,5 +247,5 @@ subject_expr: (namedexpr_test|star_expr) (',' (namedexpr_test|star_expr))* [','] # cases case_block: "case" patterns [guard] ':' suite guard: 'if' namedexpr_test -patterns: pattern ['as' pattern] -pattern: (expr|star_expr) (',' (expr|star_expr))* [','] +patterns: pattern (',' pattern)* [','] +pattern: (expr|star_expr) ['as' expr] diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index c00585e9285..b652d2685ec 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -92,3 +92,14 @@ def func(match: case, case: match) -> case: pass case {"maybe": something(complicated as this) as that}: pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass From f0a99f640279adade284eba592630c67ad374574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Tue, 21 Dec 2021 20:43:10 +0200 Subject: [PATCH 098/700] Update contributing wording (#2719) Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/contributing/the_basics.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index d9df0ea30c4..9a639731073 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -7,7 +7,8 @@ An overview on contributing to the _Black_ project. Development on the latest version of Python is preferred. As of this writing it's 3.9. You can use any operating system. -Install all development dependencies using: +Install development dependencies inside a virtual environment of your choice, for +example: ```console $ python3 -m venv .venv From ced2d656794568517ba9aa28f781f9151d89de54 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 25 Dec 2021 02:25:03 +0000 Subject: [PATCH 099/700] remove all type: ignores in src/black (GH-2720) Excet ;t --- src/black/linegen.py | 3 ++- src/black/parsing.py | 24 +++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index c1cd6fa22d9..dc238c3aee4 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -815,9 +815,10 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: # "import from" nodes store parentheses directly as part of # the statement if is_lpar_token(child): + assert is_rpar_token(node.children[-1]) # make parentheses invisible child.value = "" - node.children[-1].value = "" # type: ignore + node.children[-1].value = "" elif child.type != token.STAR: # insert invisible parentheses node.insert_child(index, Leaf(token.LPAR, "")) diff --git a/src/black/parsing.py b/src/black/parsing.py index c101643fe11..76e9de023c7 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -4,7 +4,7 @@ import ast import platform import sys -from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union +from typing import Any, AnyStr, Iterable, Iterator, List, Set, Tuple, Type, Union if sys.version_info < (3, 8): from typing_extensions import Final @@ -191,6 +191,16 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: ast27_AST: Final[Type[ast27.AST]] = ast27.AST +def _normalize(lineend: AnyStr, value: AnyStr) -> AnyStr: + # To normalize, we strip any leading and trailing space from + # each line... + stripped: List[AnyStr] = [i.strip() for i in value.splitlines()] + normalized = lineend.join(stripped) + # ...and remove any blank lines at the beginning and end of + # the whole string + return normalized.strip() + + def stringify_ast( node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0 ) -> Iterator[str]: @@ -254,14 +264,10 @@ def stringify_ast( and field == "value" and isinstance(value, (str, bytes)) ): - lineend = "\n" if isinstance(value, str) else b"\n" - # To normalize, we strip any leading and trailing space from - # each line... - stripped = [line.strip() for line in value.splitlines()] - normalized = lineend.join(stripped) # type: ignore[attr-defined] - # ...and remove any blank lines at the beginning and end of - # the whole string - normalized = normalized.strip() + if isinstance(value, str): + normalized: Union[str, bytes] = _normalize("\n", value) + else: + normalized = _normalize(b"\n", value) else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" From 092959ff1f9253347b01eeb2d6d72e15bad7e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Sat, 25 Dec 2021 04:28:43 +0100 Subject: [PATCH 100/700] Support pytest 7 by fixing broken imports (GH-2705) The tmp_path related changes are not necessary to make pytest 7 work, but it feels more complete this way. --- tests/optional.py | 11 ++++++++--- tests/test_ipynb.py | 22 +++++++++++----------- tests/test_no_ipynb.py | 8 ++++---- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/optional.py b/tests/optional.py index 1cddeeaa576..a4e9441ef1c 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -21,7 +21,12 @@ from typing import FrozenSet, List, Set, TYPE_CHECKING import pytest -from _pytest.store import StoreKey + +try: + from pytest import StashKey +except ImportError: + # pytest < 7 + from _pytest.store import StoreKey as StashKey log = logging.getLogger(__name__) @@ -33,8 +38,8 @@ from _pytest.nodes import Node -ALL_POSSIBLE_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]() -ENABLED_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]() +ALL_POSSIBLE_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]() +ENABLED_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]() def pytest_addoption(parser: "Parser") -> None: diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 141e865815a..fe8d67a7777 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -1,3 +1,4 @@ +import pathlib import re from click.testing import CliRunner @@ -12,7 +13,6 @@ import pytest from black import Mode from _pytest.monkeypatch import MonkeyPatch -from py.path import local from tests.util import DATA_DIR pytestmark = pytest.mark.jupyter @@ -371,52 +371,52 @@ def test_ipynb_diff_with_no_change() -> None: def test_cache_isnt_written_if_no_jupyter_deps_single( - monkeypatch: MonkeyPatch, tmpdir: local + monkeypatch: MonkeyPatch, tmp_path: pathlib.Path ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() nb = DATA_DIR / "notebook_trailing_newline.ipynb" - tmp_nb = tmpdir / "notebook.ipynb" + tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) monkeypatch.setattr( "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) - result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")]) + result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")]) 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(tmpdir / "notebook.ipynb")]) + result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")]) assert "reformatted" in result.output def test_cache_isnt_written_if_no_jupyter_deps_dir( - monkeypatch: MonkeyPatch, tmpdir: local + monkeypatch: MonkeyPatch, tmp_path: pathlib.Path ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() nb = DATA_DIR / "notebook_trailing_newline.ipynb" - tmp_nb = tmpdir / "notebook.ipynb" + tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) monkeypatch.setattr( "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) - result = runner.invoke(main, [str(tmpdir)]) + result = runner.invoke(main, [str(tmp_path)]) 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(tmpdir)]) + result = runner.invoke(main, [str(tmp_path)]) assert "reformatted" in result.output -def test_ipynb_flag(tmpdir: local) -> None: +def test_ipynb_flag(tmp_path: pathlib.Path) -> None: nb = DATA_DIR / "notebook_trailing_newline.ipynb" - tmp_nb = tmpdir / "notebook.a_file_extension_which_is_definitely_not_ipynb" + tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) result = runner.invoke( diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index bcda2d5369f..b03b8e13f14 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -1,10 +1,10 @@ import pytest import os +import pathlib from tests.util import THIS_DIR from black import main, jupyter_dependencies_are_installed from click.testing import CliRunner -from _pytest.tmpdir import tmpdir pytestmark = pytest.mark.no_jupyter @@ -22,14 +22,14 @@ def test_ipynb_diff_with_no_change_single() -> None: assert expected_output in result.output -def test_ipynb_diff_with_no_change_dir(tmpdir: tmpdir) -> None: +def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None: jupyter_dependencies_are_installed.cache_clear() runner = CliRunner() nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") - tmp_nb = tmpdir / "notebook.ipynb" + tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) - result = runner.invoke(main, [str(tmpdir)]) + result = runner.invoke(main, [str(tmp_path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" "You can fix this by running ``pip install black[jupyter]``\n" From b8df7e4b10bca2d7e478e224502975ec8f220e21 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 Dec 2021 22:17:11 +0100 Subject: [PATCH 101/700] Drop upper version bounds on dependencies (GH-2718) They mostly cause unnecessary trouble. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 4 ++++ setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d5cfb623c9a..cb637d94c11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,10 @@ - Unparenthesized tuples on annotated assignments (e.g `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) +### Packaging + +- All upper version bounds on dependencies have been removed (#2718) + ## 21.12b0 ### _Black_ diff --git a/setup.py b/setup.py index d314bb283f2..8ff498e4fef 100644 --- a/setup.py +++ b/setup.py @@ -99,9 +99,9 @@ def find_python_files(base: Path) -> List[Path]: install_requires=[ "click>=7.1.2", "platformdirs>=2", - "tomli>=1.1.0,<3.0.0", + "tomli>=1.1.0", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", - "pathspec>=0.9.0, <1", + "pathspec>=0.9.0", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.10.0.0", # 3.10.0.1 is broken on at least Python 3.10, From 4f5268af4f0939fbc0d5ec4fd3d113b40a4c92e9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 30 Dec 2021 19:59:53 -0500 Subject: [PATCH 102/700] Primer: exclude crashing sqlalchemy file for now (GH-2735) Until we can properly look into and fix it. -> https://github.com/psf/black/issues/2734 --- src/black_primer/primer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 2290d1df005..8c966e346d9 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -150,7 +150,11 @@ "py_versions": ["all"] }, "sqlalchemy": { - "cli_arguments": ["--experimental-string-processing"], + "cli_arguments": [ + "--experimental-string-processing", + "--extend-exclude", + "/test/orm/test_relationship_criteria.py" + ], "expect_formatting_changes": true, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", "long_checkout": false, From 8a84bebcfcabddfd5b82a8cff0b830a745999b6c Mon Sep 17 00:00:00 2001 From: Gunung Pambudi Wibisono <55311527+gunungpw@users.noreply.github.com> Date: Sun, 2 Jan 2022 10:33:20 +0700 Subject: [PATCH 103/700] Documentation: include Wing IDE 8 integrations (GH-2733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wing IDE 8 now supports autoformatting w/ Black natively 🎉 Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/integrations/editors.md | 65 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 9c279564fa3..5d2f83ace8a 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -78,36 +78,51 @@ Options include the following: ## Wing IDE -Wing supports black via the OS Commands tool, as explained in the Wing documentation on -[pep8 formatting](https://wingware.com/doc/edit/pep8). The detailed procedure is: +Wing IDE supports `black` via **Preference Settings** for system wide settings and +**Project Properties** for per-project or workspace specific settings, as explained in +the Wing documentation on +[Auto-Reformatting](https://wingware.com/doc/edit/auto-reformatting). The detailed +procedure is: -1. Install `black`. +### Prerequistes - ```console - $ pip install black - ``` +- Wing IDE version 8.0+ -1. Make sure it runs from the command line, e.g. +- Install `black`. - ```console - $ black --help - ``` + ```console + $ pip install black + ``` + +- Make sure it runs from the command line, e.g. + + ```console + $ black --help + ``` + +### Preference Settings + +If you want Wing IDE to always reformat with `black` for every project, follow these +steps: + +1. In menubar navigate to `Edit -> Preferences -> Editor -> Reformatting`. + +1. Set **Auto-Reformat** from `disable` (default) to `Line after edit` or + `Whole files before save`. + +1. Set **Reformatter** from `PEP8` (default) to `Black`. + +### Project Properties + +If you want to just reformat for a specific project and not intervene with Wing IDE +global setting, follow these steps: + +1. In menubar navigate to `Project -> Project Properties -> Options`. + +1. Set **Auto-Reformat** from `Use Preferences setting` (default) to `Line after edit` + or `Whole files before save`. -1. In Wing IDE, activate the **OS Commands** panel and define the command **black** to - execute black on the currently selected file: - - - Use the Tools -> OS Commands menu selection - - click on **+** in **OS Commands** -> New: Command line.. - - Title: black - - Command Line: black %s - - I/O Encoding: Use Default - - Key Binding: F1 - - [x] Raise OS Commands when executed - - [x] Auto-save files before execution - - [x] Line mode - -1. Select a file in the editor and press **F1** , or whatever key binding you selected - in step 3, to reformat the file. +1. Set **Reformatter** from `Use Preferences setting` (default) to `Black`. ## Vim From 668bace2aba1589aaa2bfd7c11787d79410bfd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Fri, 7 Jan 2022 18:19:03 +0200 Subject: [PATCH 104/700] Improve CLI reference wording (#2753) --- docs/usage_and_configuration/the_basics.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index d002ff0173a..fd39b6c8979 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -26,13 +26,13 @@ python -m black {source_file_or_directory} ### Command line options -_Black_ has quite a few knobs these days, although _Black_ is opinionated so style -configuration options are deliberately limited and rarely added. You can list them by -running `black --help`. +The CLI options of _Black_ can be displayed by expanding the view below or by running +`black --help`. While _Black_ has quite a few knobs these days, it is still opinionated +so style options are deliberately limited and rarely added.
-Help output +CLI reference ```{program-output} black --help From 05e1fbf27d93df36b09c560791ad46c6ce3eb518 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 7 Jan 2022 11:38:03 -0500 Subject: [PATCH 105/700] Stubs: preserve blank line between attributes and methods (#2736) --- CHANGES.md | 2 ++ src/black/lines.py | 21 +++++++++++++++++---- tests/data/stub.pyi | 27 +++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cb637d94c11..8bb96bb1f29 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,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) +- For stubs, one blank line between class attributes and methods is now kept if there's + at least one pre-existing blank line (#2736) ### Packaging diff --git a/src/black/lines.py b/src/black/lines.py index f2bdada008a..d8617d83bf7 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -448,7 +448,14 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: depth = current_line.depth while self.previous_defs and self.previous_defs[-1] >= depth: if self.is_pyi: - before = 0 if depth else 1 + assert self.previous_line is not None + if depth and not current_line.is_def and self.previous_line.is_def: + # Empty lines between attributes and methods should be preserved. + before = min(1, before) + elif depth: + before = 0 + else: + before = 1 else: if depth: before = 1 @@ -532,9 +539,15 @@ 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: - # Blank line between a block of functions (maybe with preceding - # decorators) and a block of non-functions - newlines = 1 + if not current_line.depth: + # 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: diff --git a/tests/data/stub.pyi b/tests/data/stub.pyi index 94ba852e018..9a246211284 100644 --- a/tests/data/stub.pyi +++ b/tests/data/stub.pyi @@ -2,32 +2,55 @@ X: int def f(): ... + +class D: + ... + + class C: ... class B: - ... + this_lack_of_newline_should_be_kept: int + def b(self) -> None: ... + + but_this_newline_should_also_be_kept: int class A: + attr: int + attr2: str + def f(self) -> int: ... def g(self) -> str: ... + + def g(): ... def h(): ... + # output X: int def f(): ... +class D: ... class C: ... -class B: ... + +class B: + this_lack_of_newline_should_be_kept: int + def b(self) -> None: ... + + but_this_newline_should_also_be_kept: int class A: + attr: int + attr2: str + def f(self) -> int: ... def g(self) -> str: ... From ea4c772746d787a93a0f19ce3cbabfacd8094205 Mon Sep 17 00:00:00 2001 From: Josh Owen Date: Fri, 7 Jan 2022 11:50:50 -0500 Subject: [PATCH 106/700] Action: Support running in a docker container (#2748) see: https://github.com/actions/runner/issues/716 --- CHANGES.md | 4 ++++ action.yml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8bb96bb1f29..bfecbb7831d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,10 @@ - All upper version bounds on dependencies have been removed (#2718) +### Integrations + +- Update GitHub action to support containerized runs (#2748) + ## 21.12b0 ### _Black_ diff --git a/action.yml b/action.yml index ddf07933a3e..dd2de1b62ad 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ runs: import subprocess; from pathlib import Path; - MAIN_SCRIPT = Path(r'${{ github.action_path }}') / 'action' / 'main.py'; + MAIN_SCRIPT = Path(r'${GITHUB_ACTION_PATH}') / 'action' / 'main.py'; proc = subprocess.run([sys.executable, str(MAIN_SCRIPT)]); sys.exit(proc.returncode) From e64949ee69e2a7e7f1d96331f50e801c0979a866 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 7 Jan 2022 19:51:36 +0300 Subject: [PATCH 107/700] Fix call patterns that contain as-expression on the kwargs (#2749) --- CHANGES.md | 2 ++ src/blib2to3/Grammar.txt | 2 +- tests/data/pattern_matching_extras.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index bfecbb7831d..ec2f5dc52ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ (#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) diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 27776a3b65c..cf4799f8abe 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -187,7 +187,7 @@ arglist: argument (',' argument)* [','] argument: ( test [comp_for] | test ':=' test | test 'as' test | - test '=' test | + test '=' asexpr_test | '**' test | '*' test ) diff --git a/tests/data/pattern_matching_extras.py b/tests/data/pattern_matching_extras.py index b652d2685ec..9f6907f7575 100644 --- a/tests/data/pattern_matching_extras.py +++ b/tests/data/pattern_matching_extras.py @@ -103,3 +103,17 @@ def func(match: case, case: match) -> case: case 4 as d, (5 as e), (6 | 7 as g), *h: pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {an: d, dict: 1.0}] as y, otherwise=something, q=t as u + ): + pass From e401b6bb1e1c0ed534bba59d9dc908caf7ba898c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 10 Jan 2022 07:16:30 -0500 Subject: [PATCH 108/700] Remove Python 2 support (#2740) *blib2to3's support was left untouched because: 1) I don't want to touch parsing machinery, and 2) it'll allow us to provide a more useful error message if someone does try to format Python 2 code. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- CHANGES.md | 1 + README.md | 4 +- action/main.py | 2 +- docs/faq.md | 14 +--- docs/getting_started.md | 4 +- docs/integrations/github_actions.md | 4 +- docs/the_black_code_style/current_style.md | 3 +- pyproject.toml | 1 - setup.py | 1 - src/black/__init__.py | 48 +------------ src/black/linegen.py | 9 +-- src/black/mode.py | 42 +---------- src/black/nodes.py | 10 --- src/black/numerics.py | 15 ++-- src/black/parsing.py | 82 ++++++++-------------- src/black/strings.py | 16 ++--- src/black_primer/primer.json | 11 --- src/blackd/__init__.py | 6 +- tests/data/numeric_literals_py2.py | 16 ----- tests/data/python2.py | 33 --------- tests/data/python2_print_function.py | 16 ----- tests/data/python2_unicode_literals.py | 20 ------ tests/test_black.py | 60 ---------------- tests/test_blackd.py | 7 +- tests/test_format.py | 25 ++----- tox.ini | 12 +--- 27 files changed, 73 insertions(+), 391 deletions(-) delete mode 100644 tests/data/numeric_literals_py2.py delete mode 100644 tests/data/python2.py delete mode 100755 tests/data/python2_print_function.py delete mode 100644 tests/data/python2_unicode_literals.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cb64cf9325d..48aa9291b05 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ current development version. To confirm this, you have three options: 3. Or run _Black_ on your machine: - create a new virtualenv (make sure it's the same Python version); - clone this repository; - - run `pip install -e .[d,python2]`; + - run `pip install -e .[d]`; - run `pip install -r test_requirements.txt` - make sure it's sane by running `python -m pytest`; and - run `black` like you did last time. diff --git a/CHANGES.md b/CHANGES.md index ec2f5dc52ab..bfee1b6f259 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- **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 diff --git a/README.md b/README.md index 2adf60a783a..e2b0d17ecfd 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation _Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run. If you want to format Python 2 code as well, install with -`pip install black[python2]`. If you want to format Jupyter Notebooks, install with -`pip install black[jupyter]`. +run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/action/main.py b/action/main.py index fde312553bf..d14b10f421d 100644 --- a/action/main.py +++ b/action/main.py @@ -14,7 +14,7 @@ run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True) -req = "black[colorama,python2]" +req = "black[colorama]" if VERSION: req += f"=={VERSION}" pip_proc = run( diff --git a/docs/faq.md b/docs/faq.md index 0a966c99c7f..c7d5ec33ad9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -75,16 +75,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? -```{warning} -Python 2 support has been deprecated since 21.10b0. - -This support will be dropped in the first stable release, expected for January 2022. -See [The Black Code Style](the_black_code_style/index.rst) for details. -``` - -For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra -to format Python 2 files too! In terms of running _Black_ though, Python 3.6 or newer is -required. +Support for formatting Python 2 code was removed in version 22.0. ## Why does my linter or typechecker complain after I format my code? @@ -96,8 +87,7 @@ codebase with _Black_. ## Can I run Black with PyPy? -Yes, there is support for PyPy 3.7 and higher. You cannot format Python 2 files under -PyPy, because PyPy's inbuilt ast module does not support this. +Yes, there is support for PyPy 3.7 and higher. ## Why does Black not detect syntax errors in my code? diff --git a/docs/getting_started.md b/docs/getting_started.md index c79dc607c4a..987290ac91f 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -17,9 +17,7 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation _Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run, but can format Python 2 code too. Python 2 support needs the `typed_ast` -dependency, which be installed with `pip install black[python2]`. If you want to format -Jupyter Notebooks, install with `pip install black[jupyter]`. +run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index e866a3cc616..c9697cc05de 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -8,8 +8,8 @@ environment. Great for enforcing that your code matches the _Black_ code style. This action is known to support all GitHub-hosted runner OSes. In addition, only published versions of _Black_ are supported (i.e. whatever is available on PyPI). -Finally, this action installs _Black_ with both the `colorama` and `python2` extras so -the `--color` flag and formatting Python 2 code are supported. +Finally, this action installs _Black_ with the `colorama` extra so the `--color` flag +should work fine. ## Usage diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index b9ab350cd12..68dff3eef3f 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -281,8 +281,7 @@ removed. _Black_ standardizes most numeric literals to use lowercase letters for the syntactic parts and uppercase letters for the digits themselves: `0xAB` instead of `0XAB` and -`1e10` instead of `1E10`. Python 2 long literals are styled as `2L` instead of `2l` to -avoid confusion between `l` and `1`. +`1e10` instead of `1E10`. ### Line breaks & binary operators diff --git a/pyproject.toml b/pyproject.toml index aebbc0da29c..ec617790039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] # Option below requires `tests/optional.py` optional-tests = [ - "no_python2: run when `python2` extra NOT installed", "no_blackd: run when `d` extra NOT installed", "no_jupyter: run when `jupyter` extra NOT installed", ] diff --git a/setup.py b/setup.py index 8ff498e4fef..57632498deb 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,6 @@ def find_python_files(base: Path) -> List[Path]: extras_require={ "d": ["aiohttp>=3.7.4"], "colorama": ["colorama>=0.4.3"], - "python2": ["typed-ast>=1.4.3"], "uvloop": ["uvloop>=0.15.2"], "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"], }, diff --git a/src/black/__init__.py b/src/black/__init__.py index 9bc8fc15c49..283c53f0db3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1083,20 +1083,8 @@ def f( else: versions = detect_target_versions(src_node, future_imports=future_imports) - # TODO: fully drop support and this code hopefully in January 2022 :D - if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}: - msg = ( - "DEPRECATION: Python 2 support will be removed in the first stable release " - "expected in January 2022." - ) - err(msg, fg="yellow", bold=True) - normalize_fmt_off(src_node) - lines = LineGenerator( - mode=mode, - remove_u_prefix="unicode_literals" in future_imports - or supports_feature(versions, Feature.UNICODE_LITERALS), - ) + lines = LineGenerator(mode=mode) elt = EmptyLineTracker(is_pyi=mode.is_pyi) empty_line = Line(mode=mode) after = 0 @@ -1166,14 +1154,6 @@ def get_features_used( # noqa: C901 assert isinstance(n, Leaf) if "_" in n.value: features.add(Feature.NUMERIC_UNDERSCORES) - elif n.value.endswith(("L", "l")): - # Python 2: 10L - features.add(Feature.LONG_INT_LITERAL) - elif len(n.value) >= 2 and n.value[0] == "0" and n.value[1].isdigit(): - # Python 2: 0123; 00123; ... - if not all(char == "0" for char in n.value): - # although we don't want to match 0000 or similar - features.add(Feature.OCTAL_INT_LITERAL) elif n.type == token.SLASH: if n.parent and n.parent.type in { @@ -1226,32 +1206,6 @@ def get_features_used( # noqa: C901 ): features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) - # Python 2 only features (for its deprecation) except for integers, see above - elif n.type == syms.print_stmt: - features.add(Feature.PRINT_STMT) - elif n.type == syms.exec_stmt: - features.add(Feature.EXEC_STMT) - elif n.type == syms.tfpdef: - # def set_position((x, y), value): - # ... - features.add(Feature.AUTOMATIC_PARAMETER_UNPACKING) - elif n.type == syms.except_clause: - # try: - # ... - # except Exception, err: - # ... - if len(n.children) >= 4: - if n.children[-2].type == token.COMMA: - features.add(Feature.COMMA_STYLE_EXCEPT) - elif n.type == syms.raise_stmt: - # raise Exception, "msg" - if len(n.children) >= 4: - if n.children[-2].type == token.COMMA: - features.add(Feature.COMMA_STYLE_RAISE) - elif n.type == token.BACKQUOTE: - # `i'm surprised this ever existed` - features.add(Feature.BACKQUOTE_REPR) - return features diff --git a/src/black/linegen.py b/src/black/linegen.py index dc238c3aee4..6008c773f94 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -48,9 +48,8 @@ class LineGenerator(Visitor[Line]): in ways that will no longer stringify to valid Python code on the tree. """ - def __init__(self, mode: Mode, remove_u_prefix: bool = False) -> None: + def __init__(self, mode: Mode) -> None: self.mode = mode - self.remove_u_prefix = remove_u_prefix self.current_line: Line self.__post_init__() @@ -92,9 +91,7 @@ def visit_default(self, node: LN) -> Iterator[Line]: normalize_prefix(node, inside_brackets=any_open_brackets) if self.mode.string_normalization and node.type == token.STRING: - node.value = normalize_string_prefix( - node.value, remove_u_prefix=self.remove_u_prefix - ) + node.value = normalize_string_prefix(node.value) node.value = normalize_string_quotes(node.value) if node.type == token.NUMBER: normalize_numeric_literal(node) @@ -236,7 +233,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. - docstring = normalize_string_prefix(leaf.value, self.remove_u_prefix) + docstring = normalize_string_prefix(leaf.value) prefix = get_string_prefix(docstring) docstring = docstring[len(prefix) :] # Remove the prefix quote_char = docstring[0] diff --git a/src/black/mode.py b/src/black/mode.py index bd4428add66..5e04525cfc9 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -20,7 +20,6 @@ class TargetVersion(Enum): - PY27 = 2 PY33 = 3 PY34 = 4 PY35 = 5 @@ -30,13 +29,8 @@ class TargetVersion(Enum): PY39 = 9 PY310 = 10 - def is_python2(self) -> bool: - return self is TargetVersion.PY27 - class Feature(Enum): - # All string literals are unicode - UNICODE_LITERALS = 1 F_STRINGS = 2 NUMERIC_UNDERSCORES = 3 TRAILING_COMMA_IN_CALL = 4 @@ -56,16 +50,6 @@ class Feature(Enum): # __future__ flags FUTURE_ANNOTATIONS = 51 - # temporary for Python 2 deprecation - PRINT_STMT = 200 - EXEC_STMT = 201 - AUTOMATIC_PARAMETER_UNPACKING = 202 - COMMA_STYLE_EXCEPT = 203 - COMMA_STYLE_RAISE = 204 - LONG_INT_LITERAL = 205 - OCTAL_INT_LITERAL = 206 - BACKQUOTE_REPR = 207 - FUTURE_FLAG_TO_FEATURE: Final = { "annotations": Feature.FUTURE_ANNOTATIONS, @@ -73,26 +57,10 @@ class Feature(Enum): VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { - TargetVersion.PY27: { - Feature.ASYNC_IDENTIFIERS, - Feature.PRINT_STMT, - Feature.EXEC_STMT, - Feature.AUTOMATIC_PARAMETER_UNPACKING, - Feature.COMMA_STYLE_EXCEPT, - Feature.COMMA_STYLE_RAISE, - Feature.LONG_INT_LITERAL, - Feature.OCTAL_INT_LITERAL, - Feature.BACKQUOTE_REPR, - }, - TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, - TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS}, - TargetVersion.PY35: { - Feature.UNICODE_LITERALS, - Feature.TRAILING_COMMA_IN_CALL, - Feature.ASYNC_IDENTIFIERS, - }, + TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS}, + TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS}, + TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS}, TargetVersion.PY36: { - Feature.UNICODE_LITERALS, Feature.F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, @@ -100,7 +68,6 @@ class Feature(Enum): Feature.ASYNC_IDENTIFIERS, }, TargetVersion.PY37: { - Feature.UNICODE_LITERALS, Feature.F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, @@ -109,7 +76,6 @@ class Feature(Enum): Feature.FUTURE_ANNOTATIONS, }, TargetVersion.PY38: { - Feature.UNICODE_LITERALS, Feature.F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, @@ -122,7 +88,6 @@ class Feature(Enum): Feature.ANN_ASSIGN_EXTENDED_RHS, }, TargetVersion.PY39: { - Feature.UNICODE_LITERALS, Feature.F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, @@ -136,7 +101,6 @@ class Feature(Enum): Feature.ANN_ASSIGN_EXTENDED_RHS, }, TargetVersion.PY310: { - Feature.UNICODE_LITERALS, Feature.F_STRINGS, Feature.NUMERIC_UNDERSCORES, Feature.TRAILING_COMMA_IN_CALL, diff --git a/src/black/nodes.py b/src/black/nodes.py index 75a23474024..74dfa896295 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -259,16 +259,6 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 ): return NO - elif ( - prevp.type == token.RIGHTSHIFT - and prevp.parent - and prevp.parent.type == syms.shift_expr - and prevp.prev_sibling - and is_name_token(prevp.prev_sibling) - and prevp.prev_sibling.value == "print" - ): - # Python 2 print chevron - return NO elif prevp.type == token.AT and p.parent and p.parent.type == syms.decorator: # no space in decorators return NO diff --git a/src/black/numerics.py b/src/black/numerics.py index cb1c83e7b78..879e5b2cf36 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -25,13 +25,10 @@ def format_scientific_notation(text: str) -> str: return f"{before}e{sign}{after}" -def format_long_or_complex_number(text: str) -> str: - """Formats a long or complex string like `10L` or `10j`""" +def format_complex_number(text: str) -> str: + """Formats a complex string like `10j`""" number = text[:-1] suffix = text[-1] - # Capitalize in "2L" because "l" looks too similar to "1". - if suffix == "l": - suffix = "L" return f"{format_float_or_int_string(number)}{suffix}" @@ -47,9 +44,7 @@ def format_float_or_int_string(text: str) -> str: def normalize_numeric_literal(leaf: Leaf) -> None: """Normalizes numeric (float, int, and complex) literals. - All letters used in the representation are normalized to lowercase (except - in Python 2 long literals). - """ + All letters used in the representation are normalized to lowercase.""" text = leaf.value.lower() if text.startswith(("0o", "0b")): # Leave octal and binary literals alone. @@ -58,8 +53,8 @@ def normalize_numeric_literal(leaf: Leaf) -> None: text = format_hex(text) elif "e" in text: text = format_scientific_notation(text) - elif text.endswith(("j", "l")): - text = format_long_or_complex_number(text) + elif text.endswith("j"): + text = format_complex_number(text) else: text = format_float_or_int_string(text) leaf.value = text diff --git a/src/black/parsing.py b/src/black/parsing.py index 76e9de023c7..13fa67ee84d 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -4,7 +4,7 @@ import ast import platform import sys -from typing import Any, AnyStr, Iterable, Iterator, List, Set, Tuple, Type, Union +from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union if sys.version_info < (3, 8): from typing_extensions import Final @@ -23,12 +23,11 @@ from black.nodes import syms ast3: Any -ast27: Any _IS_PYPY = platform.python_implementation() == "PyPy" try: - from typed_ast import ast3, ast27 + from typed_ast import ast3 except ImportError: # Either our python version is too low, or we're on pypy if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY): @@ -40,12 +39,11 @@ ) sys.exit(1) else: - ast3 = ast27 = ast + ast3 = ast -PY310_HINT: Final[ - str -] = "Consider using --target-version py310 to parse Python 3.10 code." +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." class InvalidInput(ValueError): @@ -60,22 +58,8 @@ 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 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, ] - if all(version.is_python2() for version in target_versions): - # Python 2-only code, so try Python 2 grammars. - return [ - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - # Python 3-compatible code, so only try Python 3 grammar. grammars = [] if supports_feature(target_versions, Feature.PATTERN_MATCHING): # Python 3.10+ @@ -129,6 +113,14 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - original_msg = exc.args[0] msg = f"{original_msg}\n{PY310_HINT}" raise InvalidInput(msg) from None + + if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar( + src_txt, pygram.python_grammar_no_print_statement + ): + original_msg = exc.args[0] + msg = f"{original_msg}\n{PY2_HINT}" + raise InvalidInput(msg) from None + raise exc from None if isinstance(result, Leaf): @@ -154,7 +146,7 @@ def lib2to3_unparse(node: Node) -> str: def parse_single_version( src: str, version: Tuple[int, int] -) -> Union[ast.AST, ast3.AST, ast27.AST]: +) -> Union[ast.AST, ast3.AST]: filename = "" # typed_ast is needed because of feature version limitations in the builtin ast if sys.version_info >= (3, 8) and version >= (3,): @@ -164,18 +156,13 @@ def parse_single_version( return ast3.parse(src, filename) else: return ast3.parse(src, filename, feature_version=version[1]) - elif version == (2, 7): - return ast27.parse(src) raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!") -def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: +def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: # TODO: support Python 4+ ;) versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)] - if ast27.__name__ != "ast": - versions.append((2, 7)) - first_error = "" for version in sorted(versions, reverse=True): try: @@ -188,22 +175,19 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: ast3_AST: Final[Type[ast3.AST]] = ast3.AST -ast27_AST: Final[Type[ast27.AST]] = ast27.AST -def _normalize(lineend: AnyStr, value: AnyStr) -> AnyStr: +def _normalize(lineend: str, value: str) -> str: # To normalize, we strip any leading and trailing space from # each line... - stripped: List[AnyStr] = [i.strip() for i in value.splitlines()] + stripped: List[str] = [i.strip() for i in value.splitlines()] normalized = lineend.join(stripped) # ...and remove any blank lines at the beginning and end of # the whole string return normalized.strip() -def stringify_ast( - node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0 -) -> Iterator[str]: +def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[str]: """Simple visitor generating strings to compare ASTs by content.""" node = fixup_ast_constants(node) @@ -215,7 +199,7 @@ def stringify_ast( # TypeIgnore will not be present using pypy < 3.8, so need for this if not (_IS_PYPY and sys.version_info < (3, 8)): # TypeIgnore has only one field 'lineno' which breaks this comparison - type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore) + type_ignore_classes = (ast3.TypeIgnore,) if sys.version_info >= (3, 8): type_ignore_classes += (ast.TypeIgnore,) if isinstance(node, type_ignore_classes): @@ -234,40 +218,34 @@ def stringify_ast( # parentheses and they change the AST. if ( field == "targets" - and isinstance(node, (ast.Delete, ast3.Delete, ast27.Delete)) - and isinstance(item, (ast.Tuple, ast3.Tuple, ast27.Tuple)) + and isinstance(node, (ast.Delete, ast3.Delete)) + and isinstance(item, (ast.Tuple, ast3.Tuple)) ): for item in item.elts: yield from stringify_ast(item, depth + 2) - elif isinstance(item, (ast.AST, ast3.AST, ast27.AST)): + elif isinstance(item, (ast.AST, ast3.AST)): yield from stringify_ast(item, depth + 2) # Note that we are referencing the typed-ast ASTs via global variables and not # direct module attribute accesses because that breaks mypyc. It's probably - # something to do with the ast3 / ast27 variables being marked as Any leading + # something to do with the ast3 variables being marked as Any leading # mypy to think this branch is always taken, leaving the rest of the code # unanalyzed. Tighting up the types for the typed-ast AST types avoids the # mypyc crash. - elif isinstance(value, (ast.AST, ast3_AST, ast27_AST)): + elif isinstance(value, (ast.AST, ast3_AST)): yield from stringify_ast(value, depth + 2) else: # Constant strings may be indented across newlines, if they are # docstrings; fold spaces after newlines when comparing. Similarly, # trailing and leading space may be removed. - # Note that when formatting Python 2 code, at least with Windows - # line-endings, docstrings can end up here as bytes instead of - # str so make sure that we handle both cases. if ( isinstance(node, ast.Constant) and field == "value" - and isinstance(value, (str, bytes)) + and isinstance(value, str) ): - if isinstance(value, str): - normalized: Union[str, bytes] = _normalize("\n", value) - else: - normalized = _normalize(b"\n", value) + normalized = _normalize("\n", value) else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" @@ -275,14 +253,12 @@ def stringify_ast( yield f"{' ' * depth}) # /{node.__class__.__name__}" -def fixup_ast_constants( - node: Union[ast.AST, ast3.AST, ast27.AST] -) -> Union[ast.AST, ast3.AST, ast27.AST]: +def fixup_ast_constants(node: Union[ast.AST, ast3.AST]) -> Union[ast.AST, ast3.AST]: """Map ast nodes deprecated in 3.8 to Constant.""" - if isinstance(node, (ast.Str, ast3.Str, ast27.Str, ast.Bytes, ast3.Bytes)): + if isinstance(node, (ast.Str, ast3.Str, ast.Bytes, ast3.Bytes)): return ast.Constant(value=node.s) - if isinstance(node, (ast.Num, ast3.Num, ast27.Num)): + if isinstance(node, (ast.Num, ast3.Num)): return ast.Constant(value=node.n) if isinstance(node, (ast.NameConstant, ast3.NameConstant)): diff --git a/src/black/strings.py b/src/black/strings.py index 06a5da01f0c..262c2ba4313 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -138,17 +138,17 @@ def assert_is_leaf_string(string: str) -> None: ), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}." -def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str: - """Make all string prefixes lowercase. - - If remove_u_prefix is given, also removes any u prefix from the string. - """ +def normalize_string_prefix(s: str) -> str: + """Make all string prefixes lowercase.""" match = STRING_PREFIX_RE.match(s) assert match is not None, f"failed to match string {s!r}" orig_prefix = match.group(1) - new_prefix = orig_prefix.replace("F", "f").replace("B", "b").replace("U", "u") - if remove_u_prefix: - new_prefix = new_prefix.replace("u", "") + new_prefix = ( + orig_prefix.replace("F", "f") + .replace("B", "b") + .replace("U", "") + .replace("u", "") + ) return f"{new_prefix}{match.group(2)}" diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 8c966e346d9..d8e13edeb06 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -149,17 +149,6 @@ "long_checkout": false, "py_versions": ["all"] }, - "sqlalchemy": { - "cli_arguments": [ - "--experimental-string-processing", - "--extend-exclude", - "/test/orm/test_relationship_criteria.py" - ], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", - "long_checkout": false, - "py_versions": ["all"] - }, "tox": { "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index cc966404a74..0463f169e19 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -174,10 +174,8 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi raise InvalidVariantHeader("major version must be 2 or 3") if len(rest) > 0: minor = int(rest[0]) - if major == 2 and minor != 7: - raise InvalidVariantHeader( - "minor version must be 7 for Python 2" - ) + if major == 2: + raise InvalidVariantHeader("Python 2 is not supported") else: # Default to lowest supported minor version. minor = 7 if major == 2 else 3 diff --git a/tests/data/numeric_literals_py2.py b/tests/data/numeric_literals_py2.py deleted file mode 100644 index 8f85c43f265..00000000000 --- a/tests/data/numeric_literals_py2.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python2.7 - -x = 123456789L -x = 123456789l -x = 123456789 -x = 0xb1acc - -# output - - -#!/usr/bin/env python2.7 - -x = 123456789L -x = 123456789L -x = 123456789 -x = 0xB1ACC diff --git a/tests/data/python2.py b/tests/data/python2.py deleted file mode 100644 index 4a22f46de42..00000000000 --- a/tests/data/python2.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python2 - -import sys - -print >> sys.stderr , "Warning:" , -print >> sys.stderr , "this is a blast from the past." -print >> sys.stderr , "Look, a repr:", `sys` - - -def function((_globals, _locals)): - exec ur"print 'hi from exec!'" in _globals, _locals - - -function((globals(), locals())) - - -# output - - -#!/usr/bin/env python2 - -import sys - -print >>sys.stderr, "Warning:", -print >>sys.stderr, "this is a blast from the past." -print >>sys.stderr, "Look, a repr:", ` sys ` - - -def function((_globals, _locals)): - exec ur"print 'hi from exec!'" in _globals, _locals - - -function((globals(), locals())) diff --git a/tests/data/python2_print_function.py b/tests/data/python2_print_function.py deleted file mode 100755 index 81b8d8a70ce..00000000000 --- a/tests/data/python2_print_function.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python2 -from __future__ import print_function - -print('hello') -print(u'hello') -print(a, file=sys.stderr) - -# output - - -#!/usr/bin/env python2 -from __future__ import print_function - -print("hello") -print(u"hello") -print(a, file=sys.stderr) diff --git a/tests/data/python2_unicode_literals.py b/tests/data/python2_unicode_literals.py deleted file mode 100644 index 2fe70392af6..00000000000 --- a/tests/data/python2_unicode_literals.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python2 -from __future__ import unicode_literals as _unicode_literals -from __future__ import absolute_import -from __future__ import print_function as lol, with_function - -u'hello' -U"hello" -Ur"hello" - -# output - - -#!/usr/bin/env python2 -from __future__ import unicode_literals as _unicode_literals -from __future__ import absolute_import -from __future__ import print_function as lol, with_function - -"hello" -"hello" -r"hello" diff --git a/tests/test_black.py b/tests/test_black.py index 628647ed977..5be4ae8533c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -724,24 +724,15 @@ def test_lib2to3_parse(self) -> None: straddling = "x + y" black.lib2to3_parse(straddling) - black.lib2to3_parse(straddling, {TargetVersion.PY27}) black.lib2to3_parse(straddling, {TargetVersion.PY36}) - black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36}) py2_only = "print x" - black.lib2to3_parse(py2_only) - black.lib2to3_parse(py2_only, {TargetVersion.PY27}) with self.assertRaises(black.InvalidInput): black.lib2to3_parse(py2_only, {TargetVersion.PY36}) - with self.assertRaises(black.InvalidInput): - black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36}) py3_only = "exec(x, end=y)" black.lib2to3_parse(py3_only) - with self.assertRaises(black.InvalidInput): - black.lib2to3_parse(py3_only, {TargetVersion.PY27}) black.lib2to3_parse(py3_only, {TargetVersion.PY36}) - black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36}) def test_get_features_used_decorator(self) -> None: # Test the feature detection of new decorator syntax @@ -1436,27 +1427,6 @@ def test_bpo_2142_workaround(self) -> None: actual = diff_header.sub(DETERMINISTIC_HEADER, actual) self.assertEqual(actual, expected) - @pytest.mark.python2 - def test_docstring_reformat_for_py27(self) -> None: - """ - Check that stripping trailing whitespace from Python 2 docstrings - doesn't trigger a "not equivalent to source" error - """ - source = ( - b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n' - ) - expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n' - - result = BlackRunner().invoke( - black.main, - ["-", "-q", "--target-version=py27"], - input=BytesIO(source), - ) - - self.assertEqual(result.exit_code, 0) - actual = result.stdout - self.assertFormatEqual(actual, expected) - @staticmethod def compare_results( result: click.testing.Result, expected_value: str, expected_exit_code: int @@ -2086,36 +2056,6 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: ) -@pytest.mark.python2 -@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"]) -def test_python_2_deprecation_with_target_version(explicit: bool) -> None: - args = [ - "--config", - str(THIS_DIR / "empty.toml"), - str(DATA_DIR / "python2.py"), - "--check", - ] - if explicit: - args.append("--target-version=py27") - with cache_dir(): - result = BlackRunner().invoke(black.main, args) - assert "DEPRECATION: Python 2 support will be removed" in result.stderr - - -@pytest.mark.python2 -def test_python_2_deprecation_autodetection_extended() -> None: - # this test has a similar construction to test_get_features_used_decorator - python2, non_python2 = read_data("python2_detection") - for python2_case in python2.split("###"): - node = black.lib2to3_parse(python2_case) - assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case - for non_python2_case in non_python2.split("###"): - node = black.lib2to3_parse(non_python2_case) - assert black.detect_target_versions(node) != { - TargetVersion.PY27 - }, non_python2_case - - try: with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() diff --git a/tests/test_blackd.py b/tests/test_blackd.py index cc750b40567..37431fcad00 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -77,6 +77,9 @@ async def check(header_value: str, expected_status: int = 400) -> None: await check("ruby3.5") await check("pyi3.6") await check("py1.5") + await check("2") + await check("2.7") + await check("py2.7") await check("2.8") await check("py2.8") await check("3.0") @@ -137,10 +140,6 @@ async def check(header_value: str, expected_status: int) -> None: await check("py36,py37", 200) await check("36", 200) await check("3.6.4", 200) - - await check("2", 204) - await check("2.7", 204) - await check("py2.7", 204) await check("3.4", 204) await check("py3.4", 204) await check("py34,py36", 204) diff --git a/tests/test_format.py b/tests/test_format.py index 30099aaf1bc..6651272a87c 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -55,12 +55,6 @@ "tupleassign", ] -SIMPLE_CASES_PY2 = [ - "numeric_literals_py2", - "python2", - "python2_unicode_literals", -] - EXPERIMENTAL_STRING_PROCESSING_CASES = [ "cantfit", "comments7", @@ -134,12 +128,6 @@ def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None: assert_format(source, expected, mode, fast=False) -@pytest.mark.parametrize("filename", SIMPLE_CASES_PY2) -@pytest.mark.python2 -def test_simple_format_py2(filename: str) -> None: - check_file(filename, DEFAULT_MODE) - - @pytest.mark.parametrize("filename", SIMPLE_CASES) def test_simple_format(filename: str) -> None: check_file(filename, DEFAULT_MODE) @@ -219,6 +207,12 @@ def test_patma_hint() -> None: 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'") + exc_info.match(black.parsing.PY2_HINT) + + def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" source, expected = read_data("docstring_no_string_normalization") @@ -245,13 +239,6 @@ def test_numeric_literals_ignoring_underscores() -> None: assert_format(source, expected, mode) -@pytest.mark.python2 -def test_python2_print_function() -> None: - source, expected = read_data("python2_print_function") - mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY27}) - assert_format(source, expected, mode) - - def test_stub() -> None: mode = replace(DEFAULT_MODE, is_pyi=True) source, expected = read_data("stub.pyi") diff --git a/tox.ini b/tox.ini index 683a5439ea9..090dc522cad 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = {,ci-}py{36,37,38,39,310,py3},fuzz setenv = PYTHONPATH = {toxinidir}/src skip_install = True # We use `recreate=True` because otherwise, on the second run of `tox -e py`, -# the `no_python2` tests would run with the Python2 extra dependencies installed. +# the `no_jupyter` tests would run with the jupyter extra dependencies installed. # See https://github.com/psf/black/issues/2367. recreate = True deps = @@ -15,15 +15,9 @@ deps = commands = pip install -e .[d] coverage erase - pytest tests --run-optional no_python2 \ - --run-optional no_jupyter \ + pytest tests --run-optional no_jupyter \ !ci: --numprocesses auto \ --cov {posargs} - pip install -e .[d,python2] - pytest tests --run-optional python2 \ - --run-optional no_jupyter \ - !ci: --numprocesses auto \ - --cov --cov-append {posargs} pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ @@ -43,7 +37,7 @@ deps = commands = pip install -e .[d] coverage erase - pytest tests --run-optional no_python2 \ + pytest tests \ --run-optional no_jupyter \ !ci: --numprocesses auto \ ci: --numprocesses 1 \ From 521d1b8129c2d83b4ab49270fe7473802259c2a2 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Mon, 10 Jan 2022 19:28:35 +0530 Subject: [PATCH 109/700] Enhance `--verbose` (#2526) Black would now echo the location that it determined as the root path for the project if `--verbose` is enabled by the user, according to which it chooses the SRC paths, i.e. the absolute path of the project is `{root}/{src}`. Closes #1880 --- CHANGES.md | 2 ++ src/black/__init__.py | 42 +++++++++++++++++++++++++++++++++++------- src/black/files.py | 28 +++++++++++++++++++--------- tests/test_black.py | 34 +++++++++++++++++++++++----------- 4 files changed, 79 insertions(+), 27 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bfee1b6f259..f6e8343ed00 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) - 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) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 283c53f0db3..cfa2c7663fe 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -31,6 +31,7 @@ ) import click +from click.core import ParameterSource from dataclasses import replace from mypy_extensions import mypyc_attr @@ -411,8 +412,37 @@ def main( config: Optional[str], ) -> None: """The uncompromising code formatter.""" - if config and verbose: - out(f"Using configuration from {config}.", bold=False, fg="blue") + ctx.ensure_object(dict) + root, method = find_project_root(src) if code is None else (None, None) + ctx.obj["root"] = root + + if verbose: + if root: + out( + f"Identified `{root}` as project root containing a {method}.", + fg="blue", + ) + + normalized = [ + (normalize_path_maybe_ignore(Path(source), root), source) + for source in src + ] + srcs_string = ", ".join( + [ + f'"{_norm}"' + if _norm + else f'\033[31m"{source} (skipping - invalid)"\033[34m' + for _norm, source in normalized + ] + ) + out(f"Sources to be formatted: {srcs_string}", fg="blue") + + if config: + config_source = ctx.get_parameter_source("config") + if config_source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP): + out("Using configuration from project root.", fg="blue") + else: + out(f"Using configuration in '{config}'.", fg="blue") error_msg = "Oh no! 💥 💔 💥" if required_version and required_version != __version__: @@ -516,14 +546,12 @@ def get_sources( stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" - - root = find_project_root(src) 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) - gitignore = get_gitignore(root) + gitignore = get_gitignore(ctx.obj["root"]) else: gitignore = None @@ -536,7 +564,7 @@ def get_sources( is_stdin = False if is_stdin or p.is_file(): - normalized_path = normalize_path_maybe_ignore(p, root, report) + normalized_path = normalize_path_maybe_ignore(p, ctx.obj["root"], report) if normalized_path is None: continue @@ -563,7 +591,7 @@ def get_sources( sources.update( gen_python_files( p.iterdir(), - root, + ctx.obj["root"], include, exclude, extend_exclude, diff --git a/src/black/files.py b/src/black/files.py index dfab9f73039..18c84237bf0 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -31,7 +31,7 @@ @lru_cache() -def find_project_root(srcs: Sequence[str]) -> Path: +def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]: """Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories @@ -39,6 +39,10 @@ def find_project_root(srcs: Sequence[str]) -> Path: If no directory in the tree contains a marker that would specify it's the project root, the root of the file system is returned. + + Returns a two-tuple with the first element as the project root path and + the second element as a string describing the method by which the + project root was discovered. """ if not srcs: srcs = [str(Path.cwd().resolve())] @@ -58,20 +62,20 @@ def find_project_root(srcs: Sequence[str]) -> Path: for directory in (common_base, *common_base.parents): if (directory / ".git").exists(): - return directory + return directory, ".git directory" if (directory / ".hg").is_dir(): - return directory + return directory, ".hg directory" if (directory / "pyproject.toml").is_file(): - return directory + return directory, "pyproject.toml" - return directory + return directory, "file system root" def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: """Find the absolute filepath to a pyproject.toml if it exists""" - path_project_root = find_project_root(path_search_start) + path_project_root, _ = find_project_root(path_search_start) path_pyproject_toml = path_project_root / "pyproject.toml" if path_pyproject_toml.is_file(): return str(path_pyproject_toml) @@ -133,7 +137,9 @@ def get_gitignore(root: Path) -> PathSpec: def normalize_path_maybe_ignore( - path: Path, root: Path, report: Report + path: Path, + root: Path, + report: Optional[Report] = None, ) -> Optional[str]: """Normalize `path`. May return `None` if `path` was ignored. @@ -143,12 +149,16 @@ def normalize_path_maybe_ignore( abspath = path if path.is_absolute() else Path.cwd() / path normalized_path = abspath.resolve().relative_to(root).as_posix() except OSError as e: - report.path_ignored(path, f"cannot be read because {e}") + if report: + report.path_ignored(path, f"cannot be read because {e}") return None except ValueError: if path.is_symlink(): - report.path_ignored(path, f"is a symbolic link that points outside {root}") + if report: + report.path_ignored( + path, f"is a symbolic link that points outside {root}" + ) return None raise diff --git a/tests/test_black.py b/tests/test_black.py index 5be4ae8533c..91d10581f6f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -100,6 +100,8 @@ class FakeContext(click.Context): def __init__(self) -> None: self.default_map: Dict[str, Any] = {} + # Dummy root, since most of the tests don't care about it + self.obj: Dict[str, Any] = {"root": PROJECT_ROOT} class FakeParameter(click.Parameter): @@ -1350,10 +1352,17 @@ def test_find_project_root(self) -> None: src_python.touch() self.assertEqual( - black.find_project_root((src_dir, test_dir)), root.resolve() + black.find_project_root((src_dir, test_dir)), + (root.resolve(), "pyproject.toml"), + ) + self.assertEqual( + black.find_project_root((src_dir,)), + (src_dir.resolve(), "pyproject.toml"), + ) + self.assertEqual( + black.find_project_root((src_python,)), + (src_dir.resolve(), "pyproject.toml"), ) - self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) - self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) @patch( "black.files.find_user_pyproject_toml", @@ -1756,6 +1765,7 @@ def assert_collected_sources( src: Sequence[Union[str, Path]], expected: Sequence[Union[str, Path]], *, + ctx: Optional[FakeContext] = None, exclude: Optional[str] = None, include: Optional[str] = None, extend_exclude: Optional[str] = None, @@ -1771,7 +1781,7 @@ def assert_collected_sources( ) gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude) collected = black.get_sources( - ctx=FakeContext(), + ctx=ctx or FakeContext(), src=gs_src, quiet=False, verbose=False, @@ -1807,9 +1817,11 @@ def test_gitignore_used_as_default(self) -> None: base / "b/.definitely_exclude/a.pyi", ] src = [base / "b/"] - assert_collected_sources(src, expected, extend_exclude=r"/exclude/") + ctx = FakeContext() + ctx.obj["root"] = base + assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/") - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_exclude_for_issue_1572(self) -> None: # Exclude shouldn't touch files that were explicitly given to Black through the # CLI. Exclude is supposed to only apply to the recursive discovery of files. @@ -1992,13 +2004,13 @@ def test_symlink_out_of_root_directory(self) -> None: child.is_symlink.assert_called() assert child.is_symlink.call_count == 2 - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin(self) -> None: src = ["-"] expected = ["-"] assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py") - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin_filename(self) -> None: src = ["-"] stdin_filename = str(THIS_DIR / "data/collections.py") @@ -2010,7 +2022,7 @@ def test_get_sources_with_stdin_filename(self) -> None: stdin_filename=stdin_filename, ) - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin_filename_and_exclude(self) -> None: # Exclude shouldn't exclude stdin_filename since it is mimicking the # file being passed directly. This is the same as @@ -2026,7 +2038,7 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None: stdin_filename=stdin_filename, ) - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: # Extend exclude shouldn't exclude stdin_filename since it is mimicking the # file being passed directly. This is the same as @@ -2042,7 +2054,7 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: stdin_filename=stdin_filename, ) - @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: # Force exclude should exclude the file when passing it through # stdin_filename From 3e731527e4418b0b6d9791d6e32caee9227ba69d Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 10 Jan 2022 21:22:00 +0300 Subject: [PATCH 110/700] Speed up new backtracking parser (#2728) --- CHANGES.md | 2 + src/blib2to3/pgen2/parse.py | 89 ++++++++++++++------ tests/data/pattern_matching_generic.py | 107 +++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 tests/data/pattern_matching_generic.py diff --git a/CHANGES.md b/CHANGES.md index f6e8343ed00..a1c8ccb0b7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,8 @@ 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) ### Packaging diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index e5dad3ae766..8fe96672897 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -46,6 +46,17 @@ def lam_sub(grammar: Grammar, node: RawNode) -> NL: return Node(type=node[0], children=node[3], context=node[2]) +# A placeholder node, used when parser is backtracking. +DUMMY_NODE = (-1, None, None, None) + + +def stack_copy( + stack: List[Tuple[DFAS, int, RawNode]] +) -> List[Tuple[DFAS, int, RawNode]]: + """Nodeless stack copy.""" + return [(copy.deepcopy(dfa), label, DUMMY_NODE) for dfa, label, _ in stack] + + class Recorder: def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> None: self.parser = parser @@ -54,7 +65,7 @@ def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> No self._dead_ilabels: Set[int] = set() self._start_point = self.parser.stack - self._points = {ilabel: copy.deepcopy(self._start_point) for ilabel in ilabels} + self._points = {ilabel: stack_copy(self._start_point) for ilabel in ilabels} @property def ilabels(self) -> Set[int]: @@ -62,13 +73,32 @@ def ilabels(self) -> Set[int]: @contextmanager def switch_to(self, ilabel: int) -> Iterator[None]: - self.parser.stack = self._points[ilabel] + with self.backtrack(): + self.parser.stack = self._points[ilabel] + try: + yield + except ParseError: + self._dead_ilabels.add(ilabel) + finally: + self.parser.stack = self._start_point + + @contextmanager + def backtrack(self) -> Iterator[None]: + """ + Use the node-level invariant ones for basic parsing operations (push/pop/shift). + These still will operate on the stack; but they won't create any new nodes, or + modify the contents of any other existing nodes. + + This saves us a ton of time when we are backtracking, since we + want to restore to the initial state as quick as possible, which + can only be done by having as little mutatations as possible. + """ + is_backtracking = self.parser.is_backtracking try: + self.parser.is_backtracking = True yield - except ParseError: - self._dead_ilabels.add(ilabel) finally: - self.parser.stack = self._start_point + self.parser.is_backtracking = is_backtracking def add_token(self, tok_type: int, tok_val: Text, raw: bool = False) -> None: func: Callable[..., Any] @@ -179,6 +209,7 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None: self.grammar = grammar # See note in docstring above. TL;DR this is ignored. self.convert = convert or lam_sub + self.is_backtracking = False def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: """Prepare for parsing. @@ -319,28 +350,40 @@ def classify(self, type: int, value: Text, context: Context) -> List[int]: def shift(self, type: int, value: Text, newstate: int, context: Context) -> None: """Shift a token. (Internal)""" - dfa, state, node = self.stack[-1] - rawnode: RawNode = (type, value, context, None) - newnode = convert(self.grammar, rawnode) - assert node[-1] is not None - node[-1].append(newnode) - self.stack[-1] = (dfa, newstate, node) + if self.is_backtracking: + dfa, state, _ = self.stack[-1] + self.stack[-1] = (dfa, newstate, DUMMY_NODE) + else: + dfa, state, node = self.stack[-1] + rawnode: RawNode = (type, value, context, None) + newnode = convert(self.grammar, rawnode) + assert node[-1] is not None + node[-1].append(newnode) + self.stack[-1] = (dfa, newstate, node) def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None: """Push a nonterminal. (Internal)""" - dfa, state, node = self.stack[-1] - newnode: RawNode = (type, None, context, []) - self.stack[-1] = (dfa, newstate, node) - self.stack.append((newdfa, 0, newnode)) + if self.is_backtracking: + dfa, state, _ = self.stack[-1] + self.stack[-1] = (dfa, newstate, DUMMY_NODE) + self.stack.append((newdfa, 0, DUMMY_NODE)) + else: + dfa, state, node = self.stack[-1] + newnode: RawNode = (type, None, context, []) + self.stack[-1] = (dfa, newstate, node) + self.stack.append((newdfa, 0, newnode)) def pop(self) -> None: """Pop a nonterminal. (Internal)""" - popdfa, popstate, popnode = self.stack.pop() - newnode = convert(self.grammar, popnode) - if self.stack: - dfa, state, node = self.stack[-1] - assert node[-1] is not None - node[-1].append(newnode) + if self.is_backtracking: + self.stack.pop() else: - self.rootnode = newnode - self.rootnode.used_names = self.used_names + popdfa, popstate, popnode = self.stack.pop() + newnode = convert(self.grammar, popnode) + if self.stack: + dfa, state, node = self.stack[-1] + assert node[-1] is not None + node[-1].append(newnode) + else: + self.rootnode = newnode + self.rootnode.used_names = self.used_names diff --git a/tests/data/pattern_matching_generic.py b/tests/data/pattern_matching_generic.py new file mode 100644 index 00000000000..00a0e4a677d --- /dev/null +++ b/tests/data/pattern_matching_generic.py @@ -0,0 +1,107 @@ +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + 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 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + 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 + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + 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) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # 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 + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" diff --git a/tests/test_format.py b/tests/test_format.py index 6651272a87c..db39678cdfe 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -69,6 +69,7 @@ "pattern_matching_complex", "pattern_matching_extras", "pattern_matching_style", + "pattern_matching_generic", "parenthesized_context_managers", ] From 0f26a0369efc7305a1a0120355f78d85b3030e56 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 10 Jan 2022 23:22:07 +0300 Subject: [PATCH 111/700] Fix handling of standalone match/case with newlines/comments (#2760) Resolves #2759 --- CHANGES.md | 2 + src/blib2to3/pgen2/parse.py | 4 ++ tests/data/pattern_matching_style.py | 68 +++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a1c8ccb0b7d..748dbca7019 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ 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) ### Packaging diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 8fe96672897..4a23d538b49 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -269,6 +269,10 @@ def addtoken(self, type: int, value: Text, context: Context) -> bool: break next_token_type, next_token_value, *_ = proxy.eat(counter) + if next_token_type in (tokenize.COMMENT, tokenize.NL): + counter += 1 + continue + if next_token_type == tokenize.OP: next_token_type = grammar.opmap[next_token_value] diff --git a/tests/data/pattern_matching_style.py b/tests/data/pattern_matching_style.py index c1c0aeedb70..8e18ce2ada6 100644 --- a/tests/data/pattern_matching_style.py +++ b/tests/data/pattern_matching_style.py @@ -6,10 +6,52 @@ ): print(1) case c( very_complex=True, - perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, ): print(2) case a: pass +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass + # output match something: @@ -20,8 +62,30 @@ ): print(1) case c( - very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, ): print(2) case a: pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass From 4efb795129b17b96bdf299eacaca4243d9af86d0 Mon Sep 17 00:00:00 2001 From: cbows <32486983+cbows@users.noreply.github.com> Date: Tue, 11 Jan 2022 18:31:07 +0100 Subject: [PATCH 112/700] Change git url for pip installation in README (#2761) * Change git url for pip installation in README Unauthenticated git protocol was disabled recently by Github and should not be used anymore. https://github.blog/2021-09-01-improving-git-protocol-security-github/#no-more-unauthenticated-git * Update CHANGES.md --- CHANGES.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 748dbca7019..565c36f8c60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,10 @@ - Update GitHub action to support containerized runs (#2748) +### Documentation + +- Change protocol in pip installation instructions to `https://` (#2761) + ## 21.12b0 ### _Black_ diff --git a/README.md b/README.md index e2b0d17ecfd..32db2bf2ce8 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ run. If you want to format Jupyter Notebooks, install with `pip install black[ju If you can't wait for the latest _hotness_ and want to install from GitHub, use: -`pip install git+git://github.com/psf/black` +`pip install git+https://github.com/psf/black` ### Usage From 8954e58ccf620a9c797f5e6ea3c53bc1f21f14d9 Mon Sep 17 00:00:00 2001 From: Jeffrey Lazar Date: Tue, 11 Jan 2022 17:37:07 -0500 Subject: [PATCH 113/700] Change installation url to comply with git security change (#2765) Co-authored-by: Jeffrey Lazar --- docs/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 987290ac91f..1227f653757 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -21,7 +21,7 @@ run. If you want to format Jupyter Notebooks, install with `pip install black[ju If you can't wait for the latest _hotness_ and want to install from GitHub, use: -`pip install git+git://github.com/psf/black` +`pip install git+https://github.com/psf/black` ## Basic usage From f298032ddb624067aebc49f792b4308aeeb1841d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 13 Jan 2022 09:33:56 -0800 Subject: [PATCH 114/700] don't expect changes on poetry (#2769) They just made themselves ESP-compliant in https://github.com/python-poetry/poetry/commit/ecb030e1f0b7c13cc11971f00ee5012e82a892bc --- src/black_primer/primer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index d8e13edeb06..a8d8fc9e21f 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -109,7 +109,7 @@ }, "poetry": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-poetry/poetry.git", "long_checkout": false, "py_versions": ["all"] From 799f76f537f72ade97b8e6637c59fee49e05a4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Thu, 13 Jan 2022 19:59:43 +0200 Subject: [PATCH 115/700] Normalise string prefix order (#2297) Closes #2171 --- CHANGES.md | 1 + docs/the_black_code_style/current_style.md | 8 +++--- src/black/strings.py | 4 +++ src/blib2to3/pgen2/tokenize.py | 2 +- tests/data/string_prefixes.py | 30 +++++++++++++--------- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 565c36f8c60..5a8a0ef9f7c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ `--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) ### Packaging diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 68dff3eef3f..11fe2c8cceb 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -233,10 +233,10 @@ _Black_ prefers double quotes (`"` and `"""`) over single quotes (`'` and `'''`) will replace the latter with the former as long as it does not result in more backslash escapes than before. -_Black_ also standardizes string prefixes, making them always lowercase. On top of that, -if your code is already Python 3.6+ only or it's using the `unicode_literals` future -import, _Black_ will remove `u` from the string prefix as it is meaningless in those -scenarios. +_Black_ also standardizes string prefixes. Prefix characters are made lowercase with the +exception of [capital "R" prefixes](#rstrings-and-rstrings), unicode literal markers +(`u`) are removed because they are meaningless in Python 3, and in the case of multiple +characters "r" is put first as in spoken language: "raw f-string". The main reason to standardize on a single form of quotes is aesthetics. Having one kind of quotes everywhere reduces reader distraction. It will also enable a future version of diff --git a/src/black/strings.py b/src/black/strings.py index 262c2ba4313..9d0e2eb8430 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -149,6 +149,10 @@ def normalize_string_prefix(s: str) -> str: .replace("U", "") .replace("u", "") ) + + # Python syntax guarantees max 2 prefixes and that one of them is "r" + if len(new_prefix) == 2 and "r" != new_prefix[0].lower(): + new_prefix = new_prefix[::-1] return f"{new_prefix}{match.group(2)}" diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index a7e17df1e8f..257dbef4a19 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -293,7 +293,7 @@ def compat(self, token: Tuple[int, Text], iterable: Iterable[TokenInfo]) -> None cookie_re = re.compile(r"^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)", re.ASCII) -blank_re = re.compile(br"^[ \t\f]*(?:[#\r\n]|$)", re.ASCII) +blank_re = re.compile(rb"^[ \t\f]*(?:[#\r\n]|$)", re.ASCII) def _get_normal_name(orig_enc: str) -> str: diff --git a/tests/data/string_prefixes.py b/tests/data/string_prefixes.py index 9ddc2b540fc..f86da696e15 100644 --- a/tests/data/string_prefixes.py +++ b/tests/data/string_prefixes.py @@ -1,10 +1,13 @@ -#!/usr/bin/env python3.6 +#!/usr/bin/env python3 -name = R"Łukasz" -F"hello {name}" -B"hello" -r"hello" -fR"hello" +name = "Łukasz" +(f"hello {name}", F"hello {name}") +(b"", B"") +(u"", U"") +(r"", R"") + +(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") +(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") def docstring_singleline(): @@ -20,13 +23,16 @@ def docstring_multiline(): # output -#!/usr/bin/env python3.6 +#!/usr/bin/env python3 + +name = "Łukasz" +(f"hello {name}", f"hello {name}") +(b"", b"") +("", "") +(r"", R"") -name = R"Łukasz" -f"hello {name}" -b"hello" -r"hello" -fR"hello" +(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") +(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") def docstring_singleline(): From 7a2956811534d7d20128ba6e911721749052b627 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 14 Jan 2022 05:01:44 +0300 Subject: [PATCH 116/700] Don't make redundant copies of the DFA (#2763) --- src/blib2to3/pgen2/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 4a23d538b49..a9dc11f39ce 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -54,7 +54,7 @@ def stack_copy( stack: List[Tuple[DFAS, int, RawNode]] ) -> List[Tuple[DFAS, int, RawNode]]: """Nodeless stack copy.""" - return [(copy.deepcopy(dfa), label, DUMMY_NODE) for dfa, label, _ in stack] + return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack] class Recorder: From 5543d1b55a2a485f2aaf32156ea97f4728264137 Mon Sep 17 00:00:00 2001 From: VanSHOE <75690289+VanSHOE@users.noreply.github.com> Date: Fri, 14 Jan 2022 08:01:08 +0530 Subject: [PATCH 117/700] Added decent coloring (#2712) --- CHANGES.md | 1 + src/black/__init__.py | 2 ++ src/black/report.py | 6 ++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5a8a0ef9f7c..6813e86e0da 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) +- 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 diff --git a/src/black/__init__.py b/src/black/__init__.py index cfa2c7663fe..fa918ce2931 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -526,6 +526,8 @@ def main( ) if verbose or not quiet: + if code is None and (verbose or report.change_count or report.failure_count): + out() out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") if code is None: click.echo(str(report), err=True) diff --git a/src/black/report.py b/src/black/report.py index 7e1c8b4b87f..43b942c9e3c 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -93,11 +93,13 @@ def __str__(self) -> str: if self.change_count: s = "s" if self.change_count > 1 else "" report.append( - style(f"{self.change_count} file{s} {reformatted}", bold=True) + style(f"{self.change_count} file{s} ", bold=True, fg="blue") + + style(f"{reformatted}", bold=True) ) + if self.same_count: s = "s" if self.same_count > 1 else "" - report.append(f"{self.same_count} file{s} {unchanged}") + report.append(style(f"{self.same_count} file{s} ", fg="blue") + unchanged) if self.failure_count: s = "s" if self.failure_count > 1 else "" report.append(style(f"{self.failure_count} file{s} {failed}", fg="red")) From 565f9c92b79a72deb7faec7503749979c791b6e1 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 13 Jan 2022 21:50:02 -0500 Subject: [PATCH 118/700] CI: add diff-shades integration (#2725) Hopefully this makes it much easier to gauge the impacts of future changes! --- .github/workflows/diff_shades.yml | 134 +++++++++++ .github/workflows/diff_shades_comment.yml | 47 ++++ .pre-commit-config.yaml | 2 +- docs/contributing/gauging_changes.md | 53 +++++ scripts/diff_shades_gha_helper.py | 272 ++++++++++++++++++++++ 5 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/diff_shades.yml create mode 100644 .github/workflows/diff_shades_comment.yml create mode 100644 scripts/diff_shades_gha_helper.py diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml new file mode 100644 index 00000000000..b6a9b3355fb --- /dev/null +++ b/.github/workflows/diff_shades.yml @@ -0,0 +1,134 @@ +name: diff-shades + +on: + push: + branches: [main] + paths-ignore: ["docs/**", "tests/**", "*.md"] + + pull_request: + path-ignore: ["docs/**", "tests/**", "*.md"] + + workflow_dispatch: + inputs: + baseline: + description: > + The baseline revision. Pro-tip, use `.pypi` to use the latest version + on PyPI or `.XXX` to use a PR. + required: true + default: main + baseline-args: + description: "Custom Black arguments (eg. -l 79)" + required: false + target: + description: > + The target revision to compare against the baseline. Same tip applies here. + required: true + target-args: + description: "Custom Black arguments (eg. -S)" + required: false + +jobs: + analysis: + name: analysis / linux + runs-on: ubuntu-latest + + steps: + - name: Checkout this repository (full clone) + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v2 + + - name: Install diff-shades and support dependencies + run: | + python -m pip install pip --upgrade + python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip + python -m pip install click packaging urllib3 + # After checking out old revisions, this might not exist so we'll use a copy. + cat scripts/diff_shades_gha_helper.py > helper.py + git config user.name "diff-shades-gha" + git config user.email "diff-shades-gha@example.com" + + - name: Calculate run configuration & metadata + id: config + env: + GITHUB_TOKEN: ${{ github.token }} + run: > + python helper.py config ${{ github.event_name }} + ${{ github.event.inputs.baseline }} ${{ github.event.inputs.target }} + --baseline-args "${{ github.event.inputs.baseline-args }}" + + - name: Attempt to use cached baseline analysis + id: baseline-cache + uses: actions/cache@v2.1.7 + with: + path: ${{ steps.config.outputs.baseline-analysis }} + key: ${{ steps.config.outputs.baseline-cache-key }} + + - name: Install baseline revision + if: steps.baseline-cache.outputs.cache-hit != 'true' + env: + GITHUB_TOKEN: ${{ github.token }} + run: ${{ steps.config.outputs.baseline-setup-cmd }} && python -m pip install . + + - name: Analyze baseline revision + if: steps.baseline-cache.outputs.cache-hit != 'true' + run: > + diff-shades analyze -v --work-dir projects-cache/ + ${{ steps.config.outputs.baseline-analysis }} -- ${{ github.event.inputs.baseline-args }} + + - name: Install target revision + env: + GITHUB_TOKEN: ${{ github.token }} + run: ${{ steps.config.outputs.target-setup-cmd }} && python -m pip install . + + - name: Analyze target revision + run: > + diff-shades analyze -v --work-dir projects-cache/ + ${{ steps.config.outputs.target-analysis }} --repeat-projects-from + ${{ steps.config.outputs.baseline-analysis }} -- ${{ github.event.inputs.target-args }} + + - name: Generate HTML diff report + run: > + diff-shades --dump-html diff.html compare --diff --quiet + ${{ steps.config.outputs.baseline-analysis }} ${{ steps.config.outputs.target-analysis }} + + - name: Upload diff report + uses: actions/upload-artifact@v2 + with: + name: diff.html + path: diff.html + + - name: Upload baseline analysis + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.config.outputs.baseline-analysis }} + path: ${{ steps.config.outputs.baseline-analysis }} + + - name: Upload target analysis + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.config.outputs.target-analysis }} + path: ${{ steps.config.outputs.target-analysis }} + + - name: Generate summary file (PR only) + if: github.event_name == 'pull_request' + run: > + python helper.py comment-body + ${{ steps.config.outputs.baseline-analysis }} ${{ steps.config.outputs.target-analysis }} + ${{ steps.config.outputs.baseline-sha }} ${{ steps.config.outputs.target-sha }} + + - name: Upload summary file (PR only) + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v2 + with: + name: .pr-comment-body.md + path: .pr-comment-body.md + + # This is last so the diff-shades-comment workflow can still work even if we + # end up detecting failed files and failing the run. + - name: Check for failed files in both analyses + run: > + diff-shades show-failed --check --show-log ${{ steps.config.outputs.baseline-analysis }}; + diff-shades show-failed --check --show-log ${{ steps.config.outputs.target-analysis }} diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml new file mode 100644 index 00000000000..bdd90321800 --- /dev/null +++ b/.github/workflows/diff_shades_comment.yml @@ -0,0 +1,47 @@ +name: diff-shades-comment + +on: + workflow_run: + workflows: [diff-shades] + types: [completed] + +permissions: + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + - name: Install support dependencies + run: | + python -m pip install pip --upgrade + python -m pip install click packaging urllib3 + + - name: Get details from initial workflow run + id: metadata + env: + GITHUB_TOKEN: ${{ github.token }} + run: > + python scripts/diff_shades_gha_helper.py comment-details + ${{github.event.workflow_run.id }} + + - name: Try to find pre-existing PR comment + if: steps.metadata.outputs.needs-comment == 'true' + id: find-comment + uses: peter-evans/find-comment@v1 + with: + issue-number: ${{ steps.metadata.outputs.pr-number }} + comment-author: "github-actions[bot]" + body-includes: "diff-shades" + + - name: Create or update PR comment + if: steps.metadata.outputs.needs-comment == 'true' + uses: peter-evans/create-or-update-comment@v1 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ steps.metadata.outputs.pr-number }} + body: ${{ steps.metadata.outputs.comment-body }} + edit-mode: replace diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52a18623612..af3c5c2b96e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: rev: v2.5.1 hooks: - id: prettier - exclude: ^Pipfile\.lock + exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index b41c7a35dda..3cfa98b3df8 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -40,3 +40,56 @@ If you're running locally yourself to test black on lots of code try: ```{program-output} black-primer --help ``` + +## 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. + +For more information, please see the [diff-shades documentation][diff-shades]. + +### CI integration + +diff-shades is also the tool behind the "diff-shades results comparing ..." / +"diff-shades reports zero changes ..." comments on PRs. The project has a GitHub Actions +workflow which runs diff-shades twice against two revisions of _Black_ according to +these rules: + +| | Baseline revision | Target revision | +| --------------------- | ----------------------- | ---------------------------- | +| On PRs | latest commit on `main` | PR commit with `main` merged | +| On pushes (main only) | latest PyPI version | the pushed commit | + +Once finished, a PR comment will be posted embedding a summary of the changes and links +to further information. If there's a pre-existing diff-shades comment, it'll be updated +instead the next time the workflow is triggered on the same PR. + +The workflow uploads 3-4 artifacts upon completion: the two generated analyses (they +have the .json file extension), `diff.html`, and `.pr-comment-body.md` if triggered by a +PR. The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be +downloaded locally. `diff.html` comes in handy for push-based or manually triggered +runs. And the analyses exist just in case you want to do further analysis using the +collected data locally. + +Note that the workflow will only fail intentionally if while analyzing a file failed to +format. Otherwise a failure indicates a bug in the workflow. + +```{tip} +Maintainers with write access or higher can trigger the workflow manually from the +Actions tab using the `workflow_dispatch` event. Simply select "diff-shades" +from the workflows list on the left, press "Run workflow", and fill in which revisions +and command line arguments to use. + +Once finished, check the logs or download the artifacts for local use. +``` + +[diff-shades]: https://github.com/ichard26/diff-shades#readme diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py new file mode 100644 index 00000000000..21e04a590a1 --- /dev/null +++ b/scripts/diff_shades_gha_helper.py @@ -0,0 +1,272 @@ +"""Helper script for psf/black's diff-shades Github Actions integration. + +diff-shades is a tool for analyzing what happens when you run Black on +OSS code capturing it for comparisons or other usage. It's used here to +help measure the impact of a change *before* landing it (in particular +posting a comment on completion for PRs). + +This script exists as a more maintainable alternative to using inline +Javascript in the workflow YAML files. The revision configuration and +resolving, caching, and PR comment logic is contained here. + +For more information, please see the developer docs: + +https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades +""" + +import json +import os +import platform +import pprint +import subprocess +import sys +import zipfile +from io import BytesIO +from pathlib import Path +from typing import Any, Dict, Optional, Tuple + +import click +import urllib3 +from packaging.version import Version + +if sys.version_info >= (3, 8): + from typing import Final, Literal +else: + from typing_extensions import Final, Literal + +COMMENT_BODY_FILE: Final = ".pr-comment-body.md" +DIFF_STEP_NAME: Final = "Generate HTML diff report" +DOCS_URL: Final = ( + "https://black.readthedocs.io/en/latest/" + "contributing/gauging_changes.html#diff-shades" +) +USER_AGENT: Final = f"psf/black diff-shades workflow via urllib3/{urllib3.__version__}" +SHA_LENGTH: Final = 10 +GH_API_TOKEN: Final = os.getenv("GITHUB_TOKEN") +REPO: Final = os.getenv("GITHUB_REPOSITORY", default="psf/black") +http = urllib3.PoolManager() + + +def set_output(name: str, value: str) -> None: + if len(value) < 200: + print(f"[INFO]: setting '{name}' to '{value}'") + else: + print(f"[INFO]: setting '{name}' to [{len(value)} chars]") + print(f"::set-output name={name}::{value}") + + +def http_get( + url: str, + is_json: bool = True, + headers: Optional[Dict[str, str]] = None, + **kwargs: Any, +) -> Any: + headers = headers or {} + headers["User-Agent"] = USER_AGENT + if "github" in url: + if GH_API_TOKEN: + headers["Authorization"] = f"token {GH_API_TOKEN}" + headers["Accept"] = "application/vnd.github.v3+json" + r = http.request("GET", url, headers=headers, **kwargs) + if is_json: + data = json.loads(r.data.decode("utf-8")) + else: + data = r.data + print(f"[INFO]: issued GET request for {r.geturl()}") + if not (200 <= r.status < 300): + pprint.pprint(dict(r.info())) + pprint.pprint(data) + raise RuntimeError(f"unexpected status code: {r.status}") + + return data + + +def get_branch_or_tag_revision(sha: str = "main") -> str: + data = http_get( + f"https://api.github.com/repos/{REPO}/commits", + fields={"per_page": "1", "sha": sha}, + ) + assert isinstance(data[0]["sha"], str) + return data[0]["sha"] + + +def get_pr_revision(pr: int) -> str: + data = http_get(f"https://api.github.com/repos/{REPO}/pulls/{pr}") + assert isinstance(data["head"]["sha"], str) + return data["head"]["sha"] + + +def get_pypi_version() -> Version: + data = http_get("https://pypi.org/pypi/black/json") + versions = [Version(v) for v in data["releases"]] + sorted_versions = sorted(versions, reverse=True) + return sorted_versions[0] + + +def resolve_custom_ref(ref: str) -> Tuple[str, str]: + if ref == ".pypi": + # Special value to get latest PyPI version. + version = str(get_pypi_version()) + return version, f"git checkout {version}" + + if ref.startswith(".") and ref[1:].isnumeric(): + # Special format to get a PR. + number = int(ref[1:]) + revision = get_pr_revision(number) + return ( + f"pr-{number}-{revision[:SHA_LENGTH]}", + f"gh pr checkout {number} && git merge origin/main", + ) + + # Alright, it's probably a branch, tag, or a commit SHA, let's find out! + revision = get_branch_or_tag_revision(ref) + # We're cutting the revision short as we might be operating on a short commit SHA. + if revision == ref or revision[: len(ref)] == ref: + # It's *probably* a commit as the resolved SHA isn't different from the REF. + return revision[:SHA_LENGTH], f"git checkout {revision}" + + # It's *probably* a pre-existing branch or tag, yay! + return f"{ref}-{revision[:SHA_LENGTH]}", f"git checkout {revision}" + + +@click.group() +def main() -> None: + pass + + +@main.command("config", help="Acquire run configuration and metadata.") +@click.argument( + "event", type=click.Choice(["push", "pull_request", "workflow_dispatch"]) +) +@click.argument("custom_baseline", required=False) +@click.argument("custom_target", required=False) +@click.option("--baseline-args", default="") +def config( + event: Literal["push", "pull_request", "workflow_dispatch"], + custom_baseline: Optional[str], + custom_target: Optional[str], + baseline_args: str, +) -> None: + import diff_shades + + if event == "push": + # Push on main, let's use PyPI Black as the baseline. + baseline_name = str(get_pypi_version()) + baseline_cmd = f"git checkout {baseline_name}" + target_rev = os.getenv("GITHUB_SHA") + assert target_rev is not None + target_name = "main-" + target_rev[:SHA_LENGTH] + target_cmd = f"git checkout {target_rev}" + + elif event == "pull_request": + # PR, let's use main as the baseline. + baseline_rev = get_branch_or_tag_revision() + baseline_name = "main-" + baseline_rev[:SHA_LENGTH] + baseline_cmd = f"git checkout {baseline_rev}" + + pr_ref = os.getenv("GITHUB_REF") + assert pr_ref is not None + pr_num = int(pr_ref[10:-6]) + pr_rev = get_pr_revision(pr_num) + target_name = f"pr-{pr_num}-{pr_rev[:SHA_LENGTH]}" + target_cmd = f"gh pr checkout {pr_num} && git merge origin/main" + + # These are only needed for the PR comment. + set_output("baseline-sha", baseline_rev) + set_output("target-sha", pr_rev) + else: + assert custom_baseline is not None and custom_target is not None + baseline_name, baseline_cmd = resolve_custom_ref(custom_baseline) + target_name, target_cmd = resolve_custom_ref(custom_target) + if baseline_name == target_name: + # Alright we're using the same revisions but we're (hopefully) using + # different command line arguments, let's support that too. + baseline_name += "-1" + target_name += "-2" + + set_output("baseline-analysis", baseline_name + ".json") + set_output("baseline-setup-cmd", baseline_cmd) + set_output("target-analysis", target_name + ".json") + set_output("target-setup-cmd", target_cmd) + + key = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}" + key += f"-{baseline_name}-{baseline_args.encode('utf-8').hex()}" + set_output("baseline-cache-key", key) + + +@main.command("comment-body", help="Generate the body for a summary PR comment.") +@click.argument("baseline", type=click.Path(exists=True, path_type=Path)) +@click.argument("target", type=click.Path(exists=True, path_type=Path)) +@click.argument("baseline-sha") +@click.argument("target-sha") +def comment_body( + baseline: Path, target: Path, baseline_sha: str, target_sha: str +) -> None: + # fmt: off + cmd = [ + sys.executable, "-m", "diff_shades", "--no-color", + "compare", str(baseline), str(target), "--quiet", "--check" + ] + # fmt: on + proc = subprocess.run(cmd, stdout=subprocess.PIPE, encoding="utf-8") + if not proc.returncode: + body = ( + f"**diff-shades** reports zero changes comparing this PR ({target_sha}) to" + f" main ({baseline_sha}).\n\n---\n\n" + ) + else: + body = ( + f"**diff-shades** results comparing this PR ({target_sha}) to main" + f" ({baseline_sha}). The full diff is [available in the logs]" + f'($job-diff-url) under the "{DIFF_STEP_NAME}" step.' + ) + body += "\n```text\n" + proc.stdout.strip() + "\n```\n" + body += ( + f"[**What is this?**]({DOCS_URL}) | [Workflow run]($workflow-run-url) |" + " [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)" + ) + print(f"[INFO]: writing half-completed comment body to {COMMENT_BODY_FILE}") + with open(COMMENT_BODY_FILE, "w", encoding="utf-8") as f: + f.write(body) + + +@main.command("comment-details", help="Get PR comment resources from a workflow run.") +@click.argument("run-id") +def comment_details(run_id: str) -> None: + data = http_get(f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}") + if data["event"] != "pull_request": + set_output("needs-comment", "false") + return + + set_output("needs-comment", "true") + pulls = data["pull_requests"] + assert len(pulls) == 1 + pr_number = pulls[0]["number"] + set_output("pr-number", str(pr_number)) + + jobs_data = http_get(data["jobs_url"]) + assert len(jobs_data["jobs"]) == 1, "multiple jobs not supported nor tested" + job = jobs_data["jobs"][0] + steps = {s["name"]: s["number"] for s in job["steps"]} + diff_step = steps[DIFF_STEP_NAME] + diff_url = job["html_url"] + f"#step:{diff_step}:1" + + artifacts_data = http_get(data["artifacts_url"])["artifacts"] + artifacts = {a["name"]: a["archive_download_url"] for a in artifacts_data} + body_url = artifacts[COMMENT_BODY_FILE] + body_zip = BytesIO(http_get(body_url, is_json=False)) + with zipfile.ZipFile(body_zip) as zfile: + with zfile.open(COMMENT_BODY_FILE) as rf: + body = rf.read().decode("utf-8") + # It's more convenient to fill in these fields after the first workflow is done + # since this command can access the workflows API (doing it in the main workflow + # while it's still in progress seems impossible). + body = body.replace("$workflow-run-url", data["html_url"]) + body = body.replace("$job-diff-url", diff_url) + # # https://github.community/t/set-output-truncates-multiline-strings/16852/3 + escaped = body.replace("%", "%25").replace("\n", "%0A").replace("\r", "%0D") + set_output("comment-body", escaped) + + +if __name__ == "__main__": + main() From 5fe6d48fcd687a972278048d3bfeec9e2040ed64 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 15 Jan 2022 04:24:55 +0000 Subject: [PATCH 119/700] Dont require typing-extensions in 3.10 (GH-2772) 3.10 ships with TypeGuard which is the newest feature we need. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + setup.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6813e86e0da..32059a30548 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,7 @@ ### Packaging - All upper version bounds on dependencies have been removed (#2718) +- `typing-extensions` is no longer a required dependency in Python 3.10+ (#2772) ### Integrations diff --git a/setup.py b/setup.py index 57632498deb..c31baab00ae 100644 --- a/setup.py +++ b/setup.py @@ -103,10 +103,7 @@ def find_python_files(base: Path) -> List[Path]: "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "pathspec>=0.9.0", "dataclasses>=0.6; python_version < '3.7'", - "typing_extensions>=3.10.0.0", - # 3.10.0.1 is broken on at least Python 3.10, - # https://github.com/python/typing/issues/865 - "typing_extensions!=3.10.0.1; python_version >= '3.10'", + "typing_extensions>=3.10.0.0; python_version < '3.10'", "mypy_extensions>=0.4.3", ], extras_require={ From 33e3bb1e4e326713f85749705179da2e31520670 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sun, 16 Jan 2022 01:19:37 +0300 Subject: [PATCH 120/700] [trivial] Use proper test cases on `unittest` (#2775) --- tests/test_black.py | 4 ++-- tests/test_primer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 91d10581f6f..202fe23ddcd 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -940,8 +940,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]") out_str = "".join(out_lines) - self.assertTrue("Expected tree:" in out_str) - self.assertTrue("Actual tree:" in out_str) + self.assertIn("Expected tree:", out_str) + self.assertIn("Actual tree:", out_str) self.assertEqual("".join(err_lines), "") @event_loop() diff --git a/tests/test_primer.py b/tests/test_primer.py index 9bb401574ca..0a9d2aec495 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -181,7 +181,7 @@ def test_gen_check_output(self) -> None: stdout, stderr = loop.run_until_complete( lib._gen_check_output([lib.BLACK_BINARY, "--help"]) ) - self.assertTrue("The uncompromising code formatter" in stdout.decode("utf8")) + self.assertIn("The uncompromising code formatter", stdout.decode("utf8")) self.assertEqual(None, stderr) # TODO: Add a test to see failure works on Windows From 1d2ed2bb421df94a8d86728a187663f1c3898322 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 06:50:27 -0800 Subject: [PATCH 121/700] Bump sphinx from 4.3.2 to 4.4.0 in /docs (#2776) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.4.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.2...v4.4.0) --- updated-dependencies: - dependency-name: sphinx 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 0cdef2a39dd..02874d3c255 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.16.1 -Sphinx==4.3.2 +Sphinx==4.4.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.4.0 From 98db4abc21477cbd247c8fbd4cf9e8d1cf61ca0f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 17 Jan 2022 07:52:29 -0800 Subject: [PATCH 122/700] Fix typo in diff_shades.yml workflow (#2778) --- .github/workflows/diff_shades.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index b6a9b3355fb..a8a443e2cce 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -6,7 +6,7 @@ on: paths-ignore: ["docs/**", "tests/**", "*.md"] pull_request: - path-ignore: ["docs/**", "tests/**", "*.md"] + paths-ignore: ["docs/**", "tests/**", "*.md"] workflow_dispatch: inputs: From 8c22d232b56104376a12d1e68eaf216d04979830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Thu, 20 Jan 2022 03:34:52 +0200 Subject: [PATCH 123/700] Create --preview CLI flag (#2752) --- CHANGES.md | 4 ++++ docs/contributing/the_basics.md | 4 +++- docs/the_black_code_style/future_style.md | 6 ++++++ docs/the_black_code_style/index.rst | 2 +- src/black/__init__.py | 10 ++++++++++ src/black/mode.py | 15 +++++++++++++++ tests/test_format.py | 17 ++++++++++++----- 7 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 32059a30548..4b9ceae81dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,10 @@ - All upper version bounds on dependencies have been removed (#2718) - `typing-extensions` is no longer a required dependency in Python 3.10+ (#2772) +### Preview style + +- Introduce the `--preview` flag with no style changes (#2752) + ### Integrations - Update GitHub action to support containerized runs (#2748) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 9a639731073..23fbb8a3d7e 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -55,7 +55,9 @@ go back and workout what to add to the `CHANGES.md` for each release. If a change would affect the advertised code style, please modify the documentation (The _Black_ code style) to reflect that change. Patches that fix unintended bugs in -formatting don't need to be mentioned separately though. +formatting don't need to be mentioned separately though. If the change is implemented +with the `--preview` flag, please include the change in the future style document +instead and write the changelog entry under a dedicated "Preview changes" heading. ### Docs Testing diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index a7676090553..70ffeefc76a 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -40,3 +40,9 @@ Currently, _Black_ does not split long strings to fit the line length limit. Cur 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). diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst index d53703277e4..3952a174223 100644 --- a/docs/the_black_code_style/index.rst +++ b/docs/the_black_code_style/index.rst @@ -32,7 +32,7 @@ versions of *Black*: improved formatting enabled by newer Python language syntax as well as due to improvements in the formatting logic. -- The ``--future`` flag is exempt from this policy. There are no guarantees +- The ``--preview`` flag is exempt from this policy. There are no guarantees around the stability of the output with that flag passed into *Black*. This flag is intended for allowing experimentation with the proposed changes to the *Black* code style. diff --git a/src/black/__init__.py b/src/black/__init__.py index fa918ce2931..405a01082e7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -246,6 +246,14 @@ def validate_regex( " Currently disabled because it leads to some crashes." ), ) +@click.option( + "--preview", + is_flag=True, + help=( + "Enable potentially disruptive style changes that will be added to Black's main" + " functionality in the next major release." + ), +) @click.option( "--check", is_flag=True, @@ -399,6 +407,7 @@ def main( skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, + preview: bool, quiet: bool, verbose: bool, required_version: Optional[str], @@ -469,6 +478,7 @@ def main( string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, + preview=preview, ) if code is not None: diff --git a/src/black/mode.py b/src/black/mode.py index 5e04525cfc9..c8c2bd4eb26 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -121,6 +121,10 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b return all(feature in VERSION_TO_FEATURES[version] for version in target_versions) +class Preview(Enum): + """Individual preview style features.""" + + @dataclass class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) @@ -130,6 +134,16 @@ class Mode: is_ipynb: bool = False magic_trailing_comma: bool = True experimental_string_processing: bool = False + preview: bool = False + + def __contains__(self, feature: Preview) -> bool: + """ + Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag. + + The argument is not checked and features are not differentiated. + They only exist to make development easier by clarifying intent. + """ + return self.preview def get_cache_key(self) -> str: if self.target_versions: @@ -147,5 +161,6 @@ def get_cache_key(self) -> str: str(int(self.is_ipynb)), str(int(self.magic_trailing_comma)), str(int(self.experimental_string_processing)), + str(int(self.preview)), ] return ".".join(parts) diff --git a/tests/test_format.py b/tests/test_format.py index db39678cdfe..00cd07f36f7 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,5 +1,5 @@ from dataclasses import replace -from typing import Any, Iterator +from typing import Any, Iterator, List from unittest.mock import patch import pytest @@ -14,7 +14,7 @@ read_data, ) -SIMPLE_CASES = [ +SIMPLE_CASES: List[str] = [ "beginning_backslash", "bracketmatch", "class_blank_parentheses", @@ -55,7 +55,7 @@ "tupleassign", ] -EXPERIMENTAL_STRING_PROCESSING_CASES = [ +EXPERIMENTAL_STRING_PROCESSING_CASES: List[str] = [ "cantfit", "comments7", "long_strings", @@ -64,7 +64,7 @@ "percent_precedence", ] -PY310_CASES = [ +PY310_CASES: List[str] = [ "pattern_matching_simple", "pattern_matching_complex", "pattern_matching_extras", @@ -73,7 +73,9 @@ "parenthesized_context_managers", ] -SOURCES = [ +PREVIEW_CASES: List[str] = [] + +SOURCES: List[str] = [ "src/black/__init__.py", "src/black/__main__.py", "src/black/brackets.py", @@ -139,6 +141,11 @@ 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)) + + @pytest.mark.parametrize("filename", SOURCES) def test_source_is_formatted(filename: str) -> None: path = THIS_DIR.parent / filename From 9bd4134f3138448eb92af7031d994b2cec7d08ad Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 19 Jan 2022 22:05:58 -0500 Subject: [PATCH 124/700] Fix and speedup diff-shades integration (#2773) --- .github/mypyc-requirements.txt | 14 ++++++ .github/workflows/diff_shades.yml | 32 ++++++++++---- .github/workflows/diff_shades_comment.yml | 4 +- docs/contributing/gauging_changes.md | 2 +- scripts/diff_shades_gha_helper.py | 54 +++++++++++------------ src/black/parsing.py | 3 +- 6 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 .github/mypyc-requirements.txt diff --git a/.github/mypyc-requirements.txt b/.github/mypyc-requirements.txt new file mode 100644 index 00000000000..4542673174c --- /dev/null +++ b/.github/mypyc-requirements.txt @@ -0,0 +1,14 @@ +mypy == 0.920 + +# A bunch of packages for type information +mypy-extensions >= 0.4.3 +tomli >= 0.10.2 +types-typed-ast >= 1.4.2 +types-dataclasses >= 0.1.3 +typing-extensions > 3.10.0.1 +click >= 8.0.0 +platformdirs >= 2.1.0 + +# And because build isolation is disabled, we'll need to pull this too +setuptools-scm[toml] >= 6.3.1 +wheel diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index a8a443e2cce..68cc2383306 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -3,10 +3,10 @@ name: diff-shades on: push: branches: [main] - paths-ignore: ["docs/**", "tests/**", "*.md"] + paths-ignore: ["docs/**", "tests/**", "**.md", "**.rst"] pull_request: - paths-ignore: ["docs/**", "tests/**", "*.md"] + paths-ignore: ["docs/**", "tests/**", "**.md", "**.rst"] workflow_dispatch: inputs: @@ -27,10 +27,18 @@ on: description: "Custom Black arguments (eg. -S)" required: false +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: analysis: name: analysis / linux runs-on: ubuntu-latest + env: + # Clang is less picky with the C code it's given than gcc (and may + # generate faster binaries too). + CC: clang-12 steps: - name: Checkout this repository (full clone) @@ -45,6 +53,7 @@ jobs: python -m pip install pip --upgrade python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip python -m pip install click packaging urllib3 + python -m pip install -r .github/mypyc-requirements.txt # After checking out old revisions, this might not exist so we'll use a copy. cat scripts/diff_shades_gha_helper.py > helper.py git config user.name "diff-shades-gha" @@ -66,11 +75,14 @@ jobs: path: ${{ steps.config.outputs.baseline-analysis }} key: ${{ steps.config.outputs.baseline-cache-key }} - - name: Install baseline revision + - name: Build and install baseline revision if: steps.baseline-cache.outputs.cache-hit != 'true' env: GITHUB_TOKEN: ${{ github.token }} - run: ${{ steps.config.outputs.baseline-setup-cmd }} && python -m pip install . + run: > + ${{ steps.config.outputs.baseline-setup-cmd }} + && python setup.py --use-mypyc bdist_wheel + && python -m pip install dist/*.whl && rm build dist -r - name: Analyze baseline revision if: steps.baseline-cache.outputs.cache-hit != 'true' @@ -78,10 +90,13 @@ jobs: diff-shades analyze -v --work-dir projects-cache/ ${{ steps.config.outputs.baseline-analysis }} -- ${{ github.event.inputs.baseline-args }} - - name: Install target revision + - name: Build and install target revision env: GITHUB_TOKEN: ${{ github.token }} - run: ${{ steps.config.outputs.target-setup-cmd }} && python -m pip install . + run: > + ${{ steps.config.outputs.target-setup-cmd }} + && python setup.py --use-mypyc bdist_wheel + && python -m pip install dist/*.whl - name: Analyze target revision run: > @@ -118,13 +133,14 @@ jobs: python helper.py comment-body ${{ steps.config.outputs.baseline-analysis }} ${{ steps.config.outputs.target-analysis }} ${{ steps.config.outputs.baseline-sha }} ${{ steps.config.outputs.target-sha }} + ${{ github.event.pull_request.number }} - name: Upload summary file (PR only) if: github.event_name == 'pull_request' uses: actions/upload-artifact@v2 with: - name: .pr-comment-body.md - path: .pr-comment-body.md + name: .pr-comment.json + path: .pr-comment.json # This is last so the diff-shades-comment workflow can still work even if we # end up detecting failed files and failing the run. diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index bdd90321800..0433bbbf85f 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -31,7 +31,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@v1 + uses: peter-evans/find-comment@d2dae40ed151c634e4189471272b57e76ec19ba8 with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" @@ -39,7 +39,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@a35cf36e5301d70b76f316e867e7788a55a31dae with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index 3cfa98b3df8..9b38fe1b628 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -74,7 +74,7 @@ to further information. If there's a pre-existing diff-shades comment, it'll be instead the next time the workflow is triggered on the same PR. The workflow uploads 3-4 artifacts upon completion: the two generated analyses (they -have the .json file extension), `diff.html`, and `.pr-comment-body.md` if triggered by a +have the .json file extension), `diff.html`, and `.pr-comment.json` if triggered by a PR. The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be downloaded locally. `diff.html` comes in handy for push-based or manually triggered runs. And the analyses exist just in case you want to do further analysis using the diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 21e04a590a1..f1f7f2be91c 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -23,7 +23,7 @@ import zipfile from io import BytesIO from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Any, Optional, Tuple import click import urllib3 @@ -34,7 +34,7 @@ else: from typing_extensions import Final, Literal -COMMENT_BODY_FILE: Final = ".pr-comment-body.md" +COMMENT_FILE: Final = ".pr-comment.json" DIFF_STEP_NAME: Final = "Generate HTML diff report" DOCS_URL: Final = ( "https://black.readthedocs.io/en/latest/" @@ -55,19 +55,16 @@ def set_output(name: str, value: str) -> None: print(f"::set-output name={name}::{value}") -def http_get( - url: str, - is_json: bool = True, - headers: Optional[Dict[str, str]] = None, - **kwargs: Any, -) -> Any: - headers = headers or {} +def http_get(url: str, is_json: bool = True, **kwargs: Any) -> Any: + headers = kwargs.get("headers") or {} headers["User-Agent"] = USER_AGENT if "github" in url: if GH_API_TOKEN: headers["Authorization"] = f"token {GH_API_TOKEN}" headers["Accept"] = "application/vnd.github.v3+json" - r = http.request("GET", url, headers=headers, **kwargs) + kwargs["headers"] = headers + + r = http.request("GET", url, **kwargs) if is_json: data = json.loads(r.data.decode("utf-8")) else: @@ -199,8 +196,9 @@ def config( @click.argument("target", type=click.Path(exists=True, path_type=Path)) @click.argument("baseline-sha") @click.argument("target-sha") +@click.argument("pr-num", type=int) def comment_body( - baseline: Path, target: Path, baseline_sha: str, target_sha: str + baseline: Path, target: Path, baseline_sha: str, target_sha: str, pr_num: int ) -> None: # fmt: off cmd = [ @@ -225,45 +223,43 @@ def comment_body( f"[**What is this?**]({DOCS_URL}) | [Workflow run]($workflow-run-url) |" " [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)" ) - print(f"[INFO]: writing half-completed comment body to {COMMENT_BODY_FILE}") - with open(COMMENT_BODY_FILE, "w", encoding="utf-8") as f: - f.write(body) + print(f"[INFO]: writing comment details to {COMMENT_FILE}") + with open(COMMENT_FILE, "w", encoding="utf-8") as f: + json.dump({"body": body, "pr-number": pr_num}, f) @main.command("comment-details", help="Get PR comment resources from a workflow run.") @click.argument("run-id") def comment_details(run_id: str) -> None: data = http_get(f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}") - if data["event"] != "pull_request": + if data["event"] != "pull_request" or data["conclusion"] == "cancelled": set_output("needs-comment", "false") return set_output("needs-comment", "true") - pulls = data["pull_requests"] - assert len(pulls) == 1 - pr_number = pulls[0]["number"] - set_output("pr-number", str(pr_number)) - - jobs_data = http_get(data["jobs_url"]) - assert len(jobs_data["jobs"]) == 1, "multiple jobs not supported nor tested" - job = jobs_data["jobs"][0] + jobs = http_get(data["jobs_url"])["jobs"] + assert len(jobs) == 1, "multiple jobs not supported nor tested" + job = jobs[0] steps = {s["name"]: s["number"] for s in job["steps"]} diff_step = steps[DIFF_STEP_NAME] diff_url = job["html_url"] + f"#step:{diff_step}:1" artifacts_data = http_get(data["artifacts_url"])["artifacts"] artifacts = {a["name"]: a["archive_download_url"] for a in artifacts_data} - body_url = artifacts[COMMENT_BODY_FILE] - body_zip = BytesIO(http_get(body_url, is_json=False)) - with zipfile.ZipFile(body_zip) as zfile: - with zfile.open(COMMENT_BODY_FILE) as rf: - body = rf.read().decode("utf-8") + comment_url = artifacts[COMMENT_FILE] + comment_zip = BytesIO(http_get(comment_url, is_json=False)) + with zipfile.ZipFile(comment_zip) as zfile: + with zfile.open(COMMENT_FILE) as rf: + comment_data = json.loads(rf.read().decode("utf-8")) + + set_output("pr-number", str(comment_data["pr-number"])) + body = comment_data["body"] # It's more convenient to fill in these fields after the first workflow is done # since this command can access the workflows API (doing it in the main workflow # while it's still in progress seems impossible). body = body.replace("$workflow-run-url", data["html_url"]) body = body.replace("$job-diff-url", diff_url) - # # https://github.community/t/set-output-truncates-multiline-strings/16852/3 + # https://github.community/t/set-output-truncates-multiline-strings/16852/3 escaped = body.replace("%", "%25").replace("\n", "%0A").replace("\r", "%0D") set_output("comment-body", escaped) diff --git a/src/black/parsing.py b/src/black/parsing.py index 13fa67ee84d..6b63368871c 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -206,7 +206,7 @@ def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[st break try: - value = getattr(node, field) + value: object = getattr(node, field) except AttributeError: continue @@ -237,6 +237,7 @@ def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[st yield from stringify_ast(value, depth + 2) else: + normalized: object # Constant strings may be indented across newlines, if they are # docstrings; fold spaces after newlines when comparing. Similarly, # trailing and leading space may be removed. 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 125/700] 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 126/700] 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 127/700] 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 128/700] 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 129/700] 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 130/700] 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 131/700] 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 132/700] 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 133/700] 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 134/700] 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 135/700] 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 136/700] 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 137/700] 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 138/700] 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 139/700] 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 140/700] 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 141/700] 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 142/700] 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 143/700] 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 144/700] 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 145/700] 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 146/700] 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 147/700] 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 148/700] 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 149/700] 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 150/700] 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 151/700] 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 152/700] 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 153/700] 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 154/700] 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 155/700] 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 156/700] 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 157/700] 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 158/700] 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 159/700] 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 160/700] 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 161/700] 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 162/700] 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 163/700] 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 164/700] 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 165/700] 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 166/700] 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 167/700] 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 168/700] 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 169/700] 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 170/700] 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 171/700] 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 172/700] 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 173/700] 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 174/700] 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: From c26c7728e883db1425f7ed7affec41da3b3200a3 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Mon, 21 Feb 2022 07:29:36 +0530 Subject: [PATCH 175/700] Add special config verbose log case when black is using user-level config (#2861) --- CHANGES.md | 2 ++ src/black/__init__.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e94b345e92a..de85bd8a02d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,8 @@ +- In verbose, mode, log when _Black_ is using user-level config (#2861) + ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 8c28b6ba18b..b7bf822ed08 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -49,7 +49,12 @@ from black.concurrency import cancel, shutdown, maybe_install_uvloop from black.output import dump_to_file, ipynb_diff, diff, color_diff, out, err from black.report import Report, Changed, NothingChanged -from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml +from black.files import ( + find_project_root, + find_pyproject_toml, + parse_pyproject_toml, + find_user_pyproject_toml, +) from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore from black.files import wrap_stream_for_windows from black.parsing import InvalidInput # noqa F401 @@ -402,7 +407,7 @@ def validate_regex( help="Read configuration from FILE path.", ) @click.pass_context -def main( +def main( # noqa: C901 ctx: click.Context, code: Optional[str], line_length: int, @@ -469,7 +474,17 @@ def main( if config: config_source = ctx.get_parameter_source("config") - if config_source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP): + user_level_config = str(find_user_pyproject_toml()) + if config == user_level_config: + out( + f"Using configuration from user-level config at " + f"'{user_level_config}'.", + fg="blue", + ) + elif config_source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): out("Using configuration from project root.", fg="blue") else: out(f"Using configuration in '{config}'.", fg="blue") From 7e2b2d4784ef8b66b2201ac936f2f9fcab936515 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 12:00:06 -0500 Subject: [PATCH 176/700] Bump furo from 2022.1.2 to 2022.2.14.1 in /docs (GH-2892) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.1.2 to 2022.2.14.1. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.01.02...2022.02.14.1) --- updated-dependencies: - dependency-name: furo 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 0b685425dde..ecd71f45e9e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ myst-parser==0.16.1 Sphinx==4.4.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.1.2 +furo==2022.2.14.1 From 2918ea3b079bbb617b2f9f0d5bc0b84fde04e48e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Feb 2022 18:20:59 -0800 Subject: [PATCH 177/700] Format ourselves in preview mode (#2889) --- pyproject.toml | 5 ++++- src/black/__init__.py | 8 ++++---- tests/test_ipynb.py | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec617790039..d9373740a5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,10 @@ extend-exclude = ''' | profiling )/ ''' - +# We use preview style for formatting Black itself. If you +# want stable formatting across releases, you should keep +# this off. +preview = true # Build system information below. # NOTE: You don't need this in your own Black configuration. diff --git a/src/black/__init__.py b/src/black/__init__.py index b7bf822ed08..c1b989536a7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1358,10 +1358,10 @@ def assert_equivalent(src: str, dst: str) -> None: src_ast = parse_ast(src) except Exception as exc: raise AssertionError( - f"cannot use --safe with this file; failed to parse source file AST: " + "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." + "This could be caused by running Black with an older Python version " + "that does not support new syntax used in your source file." ) from exc try: @@ -1380,7 +1380,7 @@ def assert_equivalent(src: str, dst: str) -> 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. Please report a bug on " + " source. Please report a bug on " f"https://github.com/psf/black/issues. This diff might be helpful: {log}" ) from None diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 473047a3b32..b534d77c22a 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -415,7 +415,7 @@ def test_ipynb_diff_with_change() -> None: f"--config={EMPTY_CONFIG}", ], ) - expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' + expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n" assert expected in result.output @@ -494,7 +494,7 @@ def test_ipynb_flag(tmp_path: pathlib.Path) -> None: f"--config={EMPTY_CONFIG}", ], ) - expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' + expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n" assert expected in result.output From 6cfb51871b59fcca04df24031f506d7dc2178a12 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Feb 2022 18:32:00 -0800 Subject: [PATCH 178/700] separate CHANGELOG section for preview style (#2890) --- CHANGES.md | 6 +++++- docs/contributing/release_process.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index de85bd8a02d..5f9341d048c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,11 @@ ### Style - + + +### Preview style + + ### _Blackd_ diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 89beb099e66..6a4b8680808 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -47,7 +47,11 @@ Use the following template for a clean changelog after the release: ### Style - + + +### Preview style + + ### _Blackd_ From 9b161072c13c0ec32c9ca9bd48fad17f781a56d4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Feb 2022 19:41:42 -0800 Subject: [PATCH 179/700] fix new formatting issue (#2895) Race between #2889 and another PR. --- 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 c1b989536a7..c4ec99b441f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -477,7 +477,7 @@ def main( # noqa: C901 user_level_config = str(find_user_pyproject_toml()) if config == user_level_config: out( - f"Using configuration from user-level config at " + "Using configuration from user-level config at " f"'{user_level_config}'.", fg="blue", ) From 147526451a43e9296ca963ed8e6b7224db93e69b Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 28 Feb 2022 20:13:34 -0800 Subject: [PATCH 180/700] README: fix "Pragmatism" link target (#2901) Fixes #2897 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ba9d6ceb98..f2d06bedad2 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ section for details). If you're feeling confident, use `--fast`. _Black_ is a PEP 8 compliant opinionated formatter. _Black_ reformats entire files in place. Style configuration options are deliberately limited and rarely added. It doesn't -take previous formatting into account (see [Pragmatism](#pragmatism) for exceptions). +take previous formatting into account (see +[Pragmatism](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#pragmatism) +for exceptions). Our documentation covers the current _Black_ code style, but planned changes to it are also documented. They're both worth taking a look: From 67eaf2466596394d5765ba4026d34e7b822814ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Jel=C3=ADnek?= Date: Thu, 3 Mar 2022 18:29:48 +0100 Subject: [PATCH 181/700] replace md5 with sha256 (#2905) MD5 is unavailable on systems with active FIPS mode. That makes black crash when run on such systems. --- CHANGES.md | 1 + src/black/mode.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5f9341d048c..b594e035b04 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,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) +- Avoid crashing when md5 is not available (#2905) ### Documentation diff --git a/src/black/mode.py b/src/black/mode.py index 6d45e3dc4da..455ed36e27e 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -4,7 +4,7 @@ chosen by the user. """ -from hashlib import md5 +from hashlib import sha256 import sys from dataclasses import dataclass, field @@ -182,6 +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(), + sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), ] return ".".join(parts) From 7af3abd38392588ac737bae09f6b15d80fe344b1 Mon Sep 17 00:00:00 2001 From: oncomouse Date: Fri, 4 Mar 2022 18:15:39 -0600 Subject: [PATCH 182/700] Move test for g:load_black to improve plugin performance (GH-2896) If a vim/neovim user wishes to suppress loading the vim plugin by setting g:load_black in their VIMRC (for me, Arch linux automatically adds the plugin to Neovim's RTP, even though I'm not using it), the current location of the test comes after a call to has('python3'). This adds, in my tests, between 35 and 45 ms to Vim load time (which I know isn't a lot but it's also unnecessary). Moving the call to `exists('g:load_black')` to before the call to `has('python3')` removes this unnecessary test and speeds up loading. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 2 ++ plugin/black.vim | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b594e035b04..a0b87c78015 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,8 @@ +- Move test to disable plugin in Vim/Neovim, which speeds up loading (#2896) + ### Output diff --git a/plugin/black.vim b/plugin/black.vim index dbe236b5f34..3fc11fe9e8d 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -15,6 +15,10 @@ " 1.2: " - use autoload script +if exists("g:load_black") + finish +endif + if v:version < 700 || !has('python3') func! __BLACK_MISSING() echo "The black.vim plugin requires vim7.0+ with Python 3.6 support." @@ -25,10 +29,6 @@ if v:version < 700 || !has('python3') finish endif -if exists("g:load_black") - finish -endif - let g:load_black = "py1.0" if !exists("g:black_virtualenv") if has("nvim") From eb213151cec22dc3589e4a033b897b216b60fd82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 16:48:53 -0800 Subject: [PATCH 183/700] Bump furo from 2022.2.14.1 to 2022.3.4 in /docs (#2906) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.2.14.1 to 2022.3.4. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.02.14.1...2022.03.04) --- updated-dependencies: - dependency-name: furo 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 ecd71f45e9e..5ca7a6f1cf7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ myst-parser==0.16.1 Sphinx==4.4.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.2.14.1 +furo==2022.3.4 From 6f4976a7ace2fb5c6a5df57c4cb7dcf65eff44c9 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sat, 5 Mar 2022 04:37:16 +0300 Subject: [PATCH 184/700] Allow `for`'s target expression to be starred (#2879) Fixes #2878 --- CHANGES.md | 3 ++- src/blib2to3/Grammar.txt | 2 +- tests/data/starred_for_target.py | 27 +++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/data/starred_for_target.py diff --git a/CHANGES.md b/CHANGES.md index a0b87c78015..ac9212e9940 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,7 +50,8 @@ ### Parser - +- Black can now parse starred expressions in the target of `for` and `async for` + statements, e.g `for item in *items_1, *items_2: pass` (#2879). ### Performance diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index cf4799f8abe..0ce6cf39111 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -109,7 +109,7 @@ compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef async_stmt: ASYNC (funcdef | with_stmt | for_stmt) if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite] while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite] -for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] +for_stmt: 'for' exprlist 'in' testlist_star_expr ':' suite ['else' ':' suite] try_stmt: ('try' ':' suite ((except_clause ':' suite)+ ['else' ':' suite] diff --git a/tests/data/starred_for_target.py b/tests/data/starred_for_target.py new file mode 100644 index 00000000000..8fc8e059ed3 --- /dev/null +++ b/tests/data/starred_for_target.py @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/tests/test_format.py b/tests/test_format.py index 04676c1c2c5..04eda43d5cf 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -62,6 +62,7 @@ ] PY310_CASES: List[str] = [ + "starred_for_target", "pattern_matching_simple", "pattern_matching_complex", "pattern_matching_extras", From dab1be38e670b822777ac5338b9b2dfef4c34690 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 08:44:01 -0800 Subject: [PATCH 185/700] Bump actions/checkout from 2 to 3 (#2909) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/changelog.yml | 2 +- .github/workflows/diff_shades.yml | 2 +- .github/workflows/diff_shades_comment.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pypi_upload.yml | 2 +- .github/workflows/test.yml | 4 ++-- .github/workflows/upload_binary.yml | 2 +- .github/workflows/uvloop_test.yml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 476e2545ce8..3ffdb086493 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Grep CHANGES.md for PR number if: contains(github.event.pull_request.labels.*.name, 'skip news') != true diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 68cc2383306..62cac6748fd 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout this repository (full clone) - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 0433bbbf85f..d09c1e3b2a3 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -12,7 +12,7 @@ jobs: comment: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v2 - name: Install support dependencies diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 5689d2887c4..b831151afac 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up latest Python uses: actions/setup-python@v2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0825385c6c0..b75ce2bb6f1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 146277a7312..4721a842773 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -18,7 +18,7 @@ jobs: python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2f6c504d3f2..0061ebcd730 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v2 diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 0921b624c45..d3eeaad286f 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ba2a84d049..72247d2bc9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Coveralls finished uses: AndreMiras/coveralls-python-action@v20201129 with: diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 766f37cc321..b2d6e05b74d 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -26,7 +26,7 @@ jobs: executable_mime: "application/x-mach-binary" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up latest Python uses: actions/setup-python@v2 diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml index a639bbd1b97..e145b16bea5 100644 --- a/.github/workflows/uvloop_test.yml +++ b/.github/workflows/uvloop_test.yml @@ -27,7 +27,7 @@ jobs: os: [ubuntu-latest, macOS-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 From fd6e92aa460659d26136f5f86878b47254480259 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:33:25 -0800 Subject: [PATCH 186/700] Bump actions/setup-python from 2 to 3 (#2908) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cooper Lees --- .github/workflows/diff_shades.yml | 2 +- .github/workflows/diff_shades_comment.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pypi_upload.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/upload_binary.yml | 2 +- .github/workflows/uvloop_test.yml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 62cac6748fd..e9deaba0136 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -46,7 +46,7 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 - name: Install diff-shades and support dependencies run: | diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index d09c1e3b2a3..cf5d8bf9ac5 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 - name: Install support dependencies run: | diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index b831151afac..1ad4b3a7605 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up latest Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 - name: Install dependencies run: | diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4721a842773..8fba67a5a01 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0061ebcd730..b630114882d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 - name: Install dependencies run: | diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index d3eeaad286f..9d970592d98 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 - name: Install latest pip, build, twine run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72247d2bc9a..ce481761aea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index b2d6e05b74d..ed8d9fdd572 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up latest Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "*" diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml index e145b16bea5..14b17d68424 100644 --- a/.github/workflows/uvloop_test.yml +++ b/.github/workflows/uvloop_test.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 - name: Install latest pip run: | From 24ffc54a53b52293a54d7ef9f2105c26e945cc67 Mon Sep 17 00:00:00 2001 From: yoerg <73831825+yoerg@users.noreply.github.com> Date: Tue, 8 Mar 2022 16:28:13 +0100 Subject: [PATCH 187/700] Fix handling of Windows junctions in normalize_path_maybe_ignore (#2904) Fixes #2569 --- CHANGES.md | 2 ++ src/black/files.py | 19 +++++++++---------- tests/test_black.py | 45 +++++++++++++++++++-------------------------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ac9212e9940..4264e631fab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,8 @@ - Black can now parse starred expressions in the target of `for` and `async for` statements, e.g `for item in *items_1, *items_2: pass` (#2879). +- Fix handling of directory junctions on Windows (#2904) + ### Performance diff --git a/src/black/files.py b/src/black/files.py index 8348e0d8c28..b6c1cf3ffe1 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -151,23 +151,22 @@ def normalize_path_maybe_ignore( """ try: abspath = path if path.is_absolute() else Path.cwd() / path - normalized_path = abspath.resolve().relative_to(root).as_posix() - except OSError as e: - if report: - report.path_ignored(path, f"cannot be read because {e}") - return None - - except ValueError: - if path.is_symlink(): + normalized_path = abspath.resolve() + try: + root_relative_path = normalized_path.relative_to(root).as_posix() + except ValueError: if report: report.path_ignored( path, f"is a symbolic link that points outside {root}" ) return None - raise + except OSError as e: + if report: + report.path_ignored(path, f"cannot be read because {e}") + return None - return normalized_path + return root_relative_path def path_is_excluded( diff --git a/tests/test_black.py b/tests/test_black.py index 82abd47dffd..b1bf1772550 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1418,6 +1418,25 @@ def test_bpo_33660_workaround(self) -> None: normalized_path = black.normalize_path_maybe_ignore(path, root, report) self.assertEqual(normalized_path, "workspace/project") + def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None: + if system() != "Windows": + return + + with TemporaryDirectory() as workspace: + root = Path(workspace) + junction_dir = root / "junction" + junction_target_outside_of_root = root / ".." + os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}") + + report = black.Report(verbose=True) + normalized_path = black.normalize_path_maybe_ignore( + junction_dir, root, report + ) + # Manually delete for Python < 3.8 + os.system(f"rmdir {junction_dir}") + + self.assertEqual(normalized_path, None) + def test_newline_comment_interaction(self) -> None: source = "class A:\\\r\n# type: ignore\n pass\n" output = black.format_str(source, mode=DEFAULT_MODE) @@ -1994,7 +2013,6 @@ def test_symlink_out_of_root_directory(self) -> None: path.iterdir.return_value = [child] child.resolve.return_value = Path("/a/b/c") child.as_posix.return_value = "/a/b/c" - child.is_symlink.return_value = True try: list( black.gen_python_files( @@ -2014,31 +2032,6 @@ def test_symlink_out_of_root_directory(self) -> None: pytest.fail(f"`get_python_files_in_dir()` failed: {ve}") path.iterdir.assert_called_once() child.resolve.assert_called_once() - child.is_symlink.assert_called_once() - # `child` should behave like a strange file which resolved path is clearly - # outside of the `root` directory. - child.is_symlink.return_value = False - with pytest.raises(ValueError): - list( - black.gen_python_files( - path.iterdir(), - root, - include, - exclude, - None, - None, - report, - gitignore, - verbose=False, - quiet=False, - ) - ) - path.iterdir.assert_called() - assert path.iterdir.call_count == 2 - child.resolve.assert_called() - assert child.resolve.call_count == 2 - child.is_symlink.assert_called() - assert child.is_symlink.call_count == 2 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin(self) -> None: From 71e71e5f52e5f6bdeae63cc8c11b1bee44d11c30 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 8 Mar 2022 08:47:51 -0800 Subject: [PATCH 188/700] Use tomllib on Python 3.11 (#2903) --- CHANGES.md | 3 +++ setup.py | 2 +- src/black/files.py | 10 +++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4264e631fab..edca0dcdad4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,9 @@ +- On Python 3.11 and newer, use the standard library's `tomllib` instead of `tomli` + (#2903) + ### Parser - Black can now parse starred expressions in the target of `for` and `async for` diff --git a/setup.py b/setup.py index 466f1a9c3a6..6b5b957e96f 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def find_python_files(base: Path) -> List[Path]: install_requires=[ "click>=8.0.0", "platformdirs>=2", - "tomli>=1.1.0", + "tomli>=1.1.0; python_version < '3.11'", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "pathspec>=0.9.0", "dataclasses>=0.6; python_version < '3.7'", diff --git a/src/black/files.py b/src/black/files.py index b6c1cf3ffe1..52c77c63346 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -20,7 +20,11 @@ from mypy_extensions import mypyc_attr from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError -import tomli + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib from black.output import err from black.report import Report @@ -97,10 +101,10 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: """Parse a pyproject toml file, pulling out relevant parts for Black - If parsing fails, will raise a tomli.TOMLDecodeError + If parsing fails, will raise a tomllib.TOMLDecodeError """ with open(path_config, "rb") as f: - pyproject_toml = tomli.load(f) + pyproject_toml = tomllib.load(f) config = pyproject_toml.get("tool", {}).get("black", {}) return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} From 9ce3c806e402abdc8a5383df0f0d1f82d930bd2e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 14 Mar 2022 19:41:46 -0400 Subject: [PATCH 189/700] Bump mypy, flake8, and pre-commit-hooks in pre-commit (GH-2922) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af3c5c2b96e..b96bc62fe17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,8 +31,8 @@ repos: files: '(CHANGES\.md|the_basics\.md)$' additional_dependencies: *version_check_dependencies - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: @@ -41,7 +41,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.940 hooks: - id: mypy exclude: ^docs/conf.py @@ -60,7 +60,7 @@ repos: exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace From a57ab326b20b720518ab6f513bd0f8ba357d8d86 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:57:59 -0400 Subject: [PATCH 190/700] Farewell black-primer, it was nice knowing you (#2924) Enjoy your retirement at https://github.com/cooperlees/black-primer --- CHANGES.md | 2 + docs/contributing/gauging_changes.md | 6 - mypy.ini | 8 - setup.py | 4 +- src/black_primer/__init__.py | 0 src/black_primer/cli.py | 195 ------------ src/black_primer/lib.py | 423 --------------------------- src/black_primer/primer.json | 181 ------------ tests/test_format.py | 3 - tests/test_primer.py | 291 ------------------ 10 files changed, 3 insertions(+), 1110 deletions(-) delete mode 100644 src/black_primer/__init__.py delete mode 100644 src/black_primer/cli.py delete mode 100644 src/black_primer/lib.py delete mode 100644 src/black_primer/primer.json delete mode 100644 tests/test_primer.py diff --git a/CHANGES.md b/CHANGES.md index edca0dcdad4..da51e94342c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,8 @@ - On Python 3.11 and newer, use the standard library's `tomllib` instead of `tomli` (#2903) +- `black-primer`, the deprecated internal devtool, has been removed and copied to a + [separate repository](https://github.com/cooperlees/black-primer) (#2924) ### Parser diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index 59c40eb3909..f28e81120b3 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -7,12 +7,6 @@ It's recommended you evaluate the quantifiable changes your _Black_ formatting modification causes before submitting a PR. Think about if the change seems disruptive enough to cause frustration to projects that are already "black formatted". -## black-primer - -`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 that runs _Black_ across a list of Git cloneable OSS projects diff --git a/mypy.ini b/mypy.ini index cfceaa3ee86..3bb92a659ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -39,11 +39,3 @@ cache_dir=/dev/null # The following is because of `patch_click()`. Remove when # we drop Python 3.6 support. warn_unused_ignores=False - -[mypy-black_primer.*] -# Until we're not supporting 3.6 primer needs this -disallow_any_generics=False - -[mypy-tests.test_primer] -# Until we're not supporting 3.6 primer needs this -disallow_any_generics=False diff --git a/setup.py b/setup.py index 6b5b957e96f..e23a58c411c 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def find_python_files(base: Path) -> List[Path]: "black/__main__.py", ] discovered = [] - # black-primer and blackd have no good reason to be compiled. + # There's no good reason for blackd to be compiled. discovered.extend(find_python_files(src / "black")) discovered.extend(find_python_files(src / "blib2to3")) mypyc_targets = [ @@ -92,7 +92,6 @@ def find_python_files(base: Path) -> List[Path]: package_data={ "blib2to3": ["*.txt"], "black": ["py.typed"], - "black_primer": ["primer.json"], }, python_requires=">=3.6.2", zip_safe=False, @@ -132,7 +131,6 @@ def find_python_files(base: Path) -> List[Path]: "console_scripts": [ "black=black:patched_main", "blackd=blackd:patched_main [d]", - "black-primer=black_primer.cli:main", ] }, ) diff --git a/src/black_primer/__init__.py b/src/black_primer/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py deleted file mode 100644 index 8524b59a632..00000000000 --- a/src/black_primer/cli.py +++ /dev/null @@ -1,195 +0,0 @@ -# coding=utf8 - -import asyncio -import json -import logging -import sys -from datetime import datetime -from pathlib import Path -from shutil import rmtree, which -from tempfile import gettempdir -from typing import Any, List, Optional, Union - -import click - -from black_primer import lib - -# If our environment has uvloop installed lets use it -try: - import uvloop - - uvloop.install() -except ImportError: - pass - - -DEFAULT_CONFIG = Path(__file__).parent / "primer.json" -_timestamp = datetime.now().strftime("%Y%m%d%H%M%S") -DEFAULT_WORKDIR = Path(gettempdir()) / f"primer.{_timestamp}" -LOG = logging.getLogger(__name__) - - -def _handle_debug( - ctx: Optional[click.core.Context], - param: Optional[Union[click.core.Option, click.core.Parameter]], - debug: Union[bool, int, str], -) -> Union[bool, int, str]: - """Turn on debugging if asked otherwise INFO default""" - log_level = logging.DEBUG if debug else logging.INFO - logging.basicConfig( - format="[%(asctime)s] %(levelname)s: %(message)s (%(filename)s:%(lineno)d)", - level=log_level, - ) - return debug - - -def load_projects(config_path: Path) -> List[str]: - with open(config_path) as config: - return sorted(json.load(config)["projects"].keys()) - - -# Unfortunately does import time file IO - but appears to be the only -# way to get `black-primer --help` to show projects list -DEFAULT_PROJECTS = load_projects(DEFAULT_CONFIG) - - -def _projects_callback( - ctx: click.core.Context, - param: Optional[Union[click.core.Option, click.core.Parameter]], - projects: str, -) -> List[str]: - requested_projects = set(projects.split(",")) - available_projects = set( - DEFAULT_PROJECTS - if str(DEFAULT_CONFIG) == ctx.params["config"] - else load_projects(ctx.params["config"]) - ) - - unavailable = requested_projects - available_projects - if unavailable: - LOG.error(f"Projects not found: {unavailable}. Available: {available_projects}") - - return sorted(requested_projects & available_projects) - - -async def async_main( - config: str, - debug: bool, - keep: bool, - long_checkouts: bool, - no_diff: bool, - projects: List[str], - rebase: bool, - workdir: str, - workers: int, -) -> int: - work_path = Path(workdir) - if not work_path.exists(): - LOG.debug(f"Creating {work_path}") - work_path.mkdir() - - if not which("black"): - LOG.error("Can not find 'black' executable in PATH. No point in running") - return -1 - - try: - ret_val = await lib.process_queue( - config, - work_path, - workers, - projects, - keep, - long_checkouts, - rebase, - no_diff, - ) - return int(ret_val) - - finally: - if not keep and work_path.exists(): - LOG.debug(f"Removing {work_path}") - rmtree(work_path, onerror=lib.handle_PermissionError) - - -@click.command(context_settings={"help_option_names": ["-h", "--help"]}) -@click.option( - "-c", - "--config", - default=str(DEFAULT_CONFIG), - type=click.Path(exists=True), - show_default=True, - help="JSON config file path", - # Eager - because config path is used by other callback options - is_eager=True, -) -@click.option( - "--debug", - is_flag=True, - callback=_handle_debug, - show_default=True, - help="Turn on debug logging", -) -@click.option( - "-k", - "--keep", - is_flag=True, - show_default=True, - help="Keep workdir + repos post run", -) -@click.option( - "-L", - "--long-checkouts", - is_flag=True, - show_default=True, - help="Pull big projects to test", -) -@click.option( - "--no-diff", - is_flag=True, - show_default=True, - help="Disable showing source file changes in black output", -) -@click.option( - "--projects", - default=",".join(DEFAULT_PROJECTS), - callback=_projects_callback, - show_default=True, - help="Comma separated list of projects to run", -) -@click.option( - "-R", - "--rebase", - is_flag=True, - show_default=True, - help="Rebase project if already checked out", -) -@click.option( - "-w", - "--workdir", - default=str(DEFAULT_WORKDIR), - type=click.Path(exists=False), - show_default=True, - help="Directory path for repo checkouts", -) -@click.option( - "-W", - "--workers", - default=2, - type=int, - show_default=True, - help="Number of parallel worker coroutines", -) -@click.pass_context -def main(ctx: click.core.Context, **kwargs: Any) -> None: - """primer - prime projects for blackening... 🏴""" - LOG.debug(f"Starting {sys.argv[0]}") - # TODO: Change to asyncio.run when Black >= 3.7 only - loop = asyncio.get_event_loop() - try: - ctx.exit(loop.run_until_complete(async_main(**kwargs))) - finally: - loop.close() - - -if __name__ == "__main__": # pragma: nocover - main() diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py deleted file mode 100644 index 13724f431ce..00000000000 --- a/src/black_primer/lib.py +++ /dev/null @@ -1,423 +0,0 @@ -import asyncio -import errno -import json -import logging -import os -import stat -import sys -from functools import partial -from pathlib import Path -from platform import system -from shutil import rmtree, which -from subprocess import CalledProcessError -from sys import version_info -from tempfile import TemporaryDirectory -from typing import ( - Any, - Callable, - Dict, - List, - NamedTuple, - Optional, - Sequence, - Tuple, - Union, -) -from urllib.parse import urlparse - -import click - - -TEN_MINUTES_SECONDS = 600 -WINDOWS = system() == "Windows" -BLACK_BINARY = "black.exe" if WINDOWS else "black" -GIT_BINARY = "git.exe" if WINDOWS else "git" -LOG = logging.getLogger(__name__) - - -# Windows needs a ProactorEventLoop if you want to exec subprocesses -# Starting with 3.8 this is the default - can remove when Black >= 3.8 -# mypy only respects sys.platform if directly in the evaluation -# https://mypy.readthedocs.io/en/latest/common_issues.html#python-version-and-system-platform-checks # noqa: B950 -if sys.platform == "win32": - asyncio.set_event_loop(asyncio.ProactorEventLoop()) - - -class Results(NamedTuple): - stats: Dict[str, int] = {} - failed_projects: Dict[str, CalledProcessError] = {} - - -async def _gen_check_output( - cmd: Sequence[str], - timeout: float = TEN_MINUTES_SECONDS, - env: Optional[Dict[str, str]] = None, - cwd: Optional[Path] = None, - stdin: Optional[bytes] = None, -) -> Tuple[bytes, bytes]: - process = await asyncio.create_subprocess_exec( - *cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - env=env, - cwd=cwd, - ) - try: - (stdout, stderr) = await asyncio.wait_for(process.communicate(stdin), timeout) - except asyncio.TimeoutError: - process.kill() - await process.wait() - raise - - # A non-optional timeout was supplied to asyncio.wait_for, guaranteeing - # a timeout or completed process. A terminated Python process will have a - # non-empty returncode value. - assert process.returncode is not None - - if process.returncode != 0: - cmd_str = " ".join(cmd) - raise CalledProcessError( - process.returncode, cmd_str, output=stdout, stderr=stderr - ) - - return (stdout, stderr) - - -def analyze_results(project_count: int, results: Results) -> int: - failed_pct = round(((results.stats["failed"] / project_count) * 100), 2) - success_pct = round(((results.stats["success"] / project_count) * 100), 2) - - if results.failed_projects: - click.secho("\nFailed projects:\n", bold=True) - - for project_name, project_cpe in results.failed_projects.items(): - print(f"## {project_name}:") - print(f" - Returned {project_cpe.returncode}") - if project_cpe.stderr: - print(f" - stderr:\n{project_cpe.stderr.decode('utf8')}") - if project_cpe.stdout: - print(f" - stdout:\n{project_cpe.stdout.decode('utf8')}") - print("") - - click.secho("-- primer results 📊 --\n", bold=True) - click.secho( - f"{results.stats['success']} / {project_count} succeeded ({success_pct}%) ✅", - bold=True, - fg="green", - ) - click.secho( - f"{results.stats['failed']} / {project_count} FAILED ({failed_pct}%) 💩", - bold=bool(results.stats["failed"]), - fg="red", - ) - s = "" if results.stats["disabled"] == 1 else "s" - click.echo(f" - {results.stats['disabled']} project{s} disabled by config") - s = "" if results.stats["wrong_py_ver"] == 1 else "s" - click.echo( - f" - {results.stats['wrong_py_ver']} project{s} skipped due to Python version" - ) - click.echo( - f" - {results.stats['skipped_long_checkout']} skipped due to long checkout" - ) - - if results.failed_projects: - failed = ", ".join(results.failed_projects.keys()) - click.secho(f"\nFailed projects: {failed}\n", bold=True) - - return results.stats["failed"] - - -def _flatten_cli_args(cli_args: List[Union[Sequence[str], str]]) -> List[str]: - """Allow a user to put long arguments into a list of strs - to make the JSON human readable""" - flat_args = [] - for arg in cli_args: - if isinstance(arg, str): - flat_args.append(arg) - continue - - args_as_str = "".join(arg) - flat_args.append(args_as_str) - - return flat_args - - -async def black_run( - project_name: str, - repo_path: Optional[Path], - project_config: Dict[str, Any], - results: Results, - no_diff: bool = False, -) -> None: - """Run Black and record failures""" - if not repo_path: - results.stats["failed"] += 1 - results.failed_projects[project_name] = CalledProcessError( - 69, [], f"{project_name} has no repo_path: {repo_path}".encode(), b"" - ) - return - - stdin_test = project_name.upper() == "STDIN" - cmd = [str(which(BLACK_BINARY))] - if "cli_arguments" in project_config and project_config["cli_arguments"]: - cmd.extend(_flatten_cli_args(project_config["cli_arguments"])) - cmd.append("--check") - if not no_diff: - cmd.append("--diff") - - # Workout if we should read in a python file or search from cwd - stdin = None - if stdin_test: - cmd.append("-") - stdin = repo_path.read_bytes() - elif "base_path" in project_config: - cmd.append(project_config["base_path"]) - else: - cmd.append(".") - - timeout = ( - project_config["timeout_seconds"] - if "timeout_seconds" in project_config - else TEN_MINUTES_SECONDS - ) - with TemporaryDirectory() as tmp_path: - # Prevent reading top-level user configs by manipulating environment variables - env = { - **os.environ, - "XDG_CONFIG_HOME": tmp_path, # Unix-like - "USERPROFILE": tmp_path, # Windows (changes `Path.home()` output) - } - - cwd_path = repo_path.parent if stdin_test else repo_path - try: - LOG.debug(f"Running black for {project_name}: {' '.join(cmd)}") - _stdout, _stderr = await _gen_check_output( - cmd, cwd=cwd_path, env=env, stdin=stdin, timeout=timeout - ) - except asyncio.TimeoutError: - results.stats["failed"] += 1 - LOG.error(f"Running black for {repo_path} timed out ({cmd})") - except CalledProcessError as cpe: - # TODO: Tune for smarter for higher signal - # If any other return value than 1 we raise - can disable project in config - if cpe.returncode == 1: - if not project_config["expect_formatting_changes"]: - results.stats["failed"] += 1 - results.failed_projects[repo_path.name] = cpe - else: - results.stats["success"] += 1 - return - elif cpe.returncode > 1: - results.stats["failed"] += 1 - results.failed_projects[repo_path.name] = cpe - return - - LOG.error(f"Unknown error with {repo_path}") - raise - - # If we get here and expect formatting changes something is up - if project_config["expect_formatting_changes"]: - results.stats["failed"] += 1 - results.failed_projects[repo_path.name] = CalledProcessError( - 0, cmd, b"Expected formatting changes but didn't get any!", b"" - ) - return - - results.stats["success"] += 1 - - -async def git_checkout_or_rebase( - work_path: Path, - project_config: Dict[str, Any], - rebase: bool = False, - *, - depth: int = 1, -) -> Optional[Path]: - """git Clone project or rebase""" - git_bin = str(which(GIT_BINARY)) - if not git_bin: - LOG.error("No git binary found") - return None - - repo_url_parts = urlparse(project_config["git_clone_url"]) - path_parts = repo_url_parts.path[1:].split("/", maxsplit=1) - - repo_path: Path = work_path / path_parts[1].replace(".git", "") - cmd = [git_bin, "clone", "--depth", str(depth), project_config["git_clone_url"]] - cwd = work_path - if repo_path.exists() and rebase: - cmd = [git_bin, "pull", "--rebase"] - cwd = repo_path - elif repo_path.exists(): - return repo_path - - try: - _stdout, _stderr = await _gen_check_output(cmd, cwd=cwd) - except (asyncio.TimeoutError, CalledProcessError) as e: - LOG.error(f"Unable to git clone / pull {project_config['git_clone_url']}: {e}") - return None - - return repo_path - - -def handle_PermissionError( - func: Callable[..., None], path: Path, exc: Tuple[Any, Any, Any] -) -> None: - """ - Handle PermissionError during shutil.rmtree. - - This checks if the erroring function is either 'os.rmdir' or 'os.unlink', and that - the error was EACCES (i.e. Permission denied). If true, the path is set writable, - readable, and executable by everyone. Finally, it tries the error causing delete - operation again. - - If the check is false, then the original error will be reraised as this function - can't handle it. - """ - excvalue = exc[1] - LOG.debug(f"Handling {excvalue} from {func.__name__}... ") - if func in (os.rmdir, os.unlink) and excvalue.errno == errno.EACCES: - LOG.debug(f"Setting {path} writable, readable, and executable by everyone... ") - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # chmod 0777 - func(path) # Try the error causing delete operation again - else: - raise - - -async def load_projects_queue( - config_path: Path, - projects_to_run: List[str], -) -> Tuple[Dict[str, Any], asyncio.Queue]: - """Load project config and fill queue with all the project names""" - with config_path.open("r") as cfp: - config = json.load(cfp) - - # TODO: Offer more options here - # e.g. Run on X random packages etc. - queue: asyncio.Queue = asyncio.Queue(maxsize=len(projects_to_run)) - for project in projects_to_run: - await queue.put(project) - - return config, queue - - -async def project_runner( - idx: int, - config: Dict[str, Any], - queue: asyncio.Queue, - work_path: Path, - results: Results, - long_checkouts: bool = False, - rebase: bool = False, - keep: bool = False, - no_diff: bool = False, -) -> None: - """Check out project and run Black on it + record result""" - loop = asyncio.get_event_loop() - py_version = f"{version_info[0]}.{version_info[1]}" - while True: - try: - project_name = queue.get_nowait() - except asyncio.QueueEmpty: - LOG.debug(f"project_runner {idx} exiting") - return - LOG.debug(f"worker {idx} working on {project_name}") - - project_config = config["projects"][project_name] - - # Check if disabled by config - if "disabled" in project_config and project_config["disabled"]: - results.stats["disabled"] += 1 - LOG.info(f"Skipping {project_name} as it's disabled via config") - continue - - # Check if we should run on this version of Python - if ( - "all" not in project_config["py_versions"] - and py_version not in project_config["py_versions"] - ): - results.stats["wrong_py_ver"] += 1 - LOG.debug(f"Skipping {project_name} as it's not enabled for {py_version}") - continue - - # Check if we're doing big projects / long checkouts - if not long_checkouts and project_config["long_checkout"]: - results.stats["skipped_long_checkout"] += 1 - LOG.debug(f"Skipping {project_name} as it's configured as a long checkout") - continue - - repo_path: Optional[Path] = Path(__file__) - stdin_project = project_name.upper() == "STDIN" - if not stdin_project: - repo_path = await git_checkout_or_rebase(work_path, project_config, rebase) - if not repo_path: - continue - await black_run(project_name, repo_path, project_config, results, no_diff) - - if not keep and not stdin_project: - LOG.debug(f"Removing {repo_path}") - rmtree_partial = partial( - rmtree, path=repo_path, onerror=handle_PermissionError - ) - await loop.run_in_executor(None, rmtree_partial) - - LOG.info(f"Finished {project_name}") - - -async def process_queue( - config_file: str, - work_path: Path, - workers: int, - projects_to_run: List[str], - keep: bool = False, - long_checkouts: bool = False, - rebase: bool = False, - no_diff: bool = False, -) -> int: - """ - Process the queue with X workers and evaluate results - - Success is guaged via the config "expect_formatting_changes" - - Integer return equals the number of failed projects - """ - results = Results() - results.stats["disabled"] = 0 - results.stats["failed"] = 0 - results.stats["skipped_long_checkout"] = 0 - results.stats["success"] = 0 - results.stats["wrong_py_ver"] = 0 - - config, queue = await load_projects_queue(Path(config_file), projects_to_run) - project_count = queue.qsize() - s = "" if project_count == 1 else "s" - LOG.info(f"{project_count} project{s} to run Black over") - if project_count < 1: - return -1 - - s = "" if workers == 1 else "s" - LOG.debug(f"Using {workers} parallel worker{s} to run Black") - # Wait until we finish running all the projects before analyzing - await asyncio.gather( - *[ - project_runner( - i, - config, - queue, - work_path, - results, - long_checkouts, - rebase, - keep, - no_diff, - ) - for i in range(workers) - ] - ) - - LOG.info("Analyzing results") - return analyze_results(project_count, results) - - -if __name__ == "__main__": # pragma: nocover - raise NotImplementedError("lib is a library, funnily enough.") diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json deleted file mode 100644 index a6bfd4a2fec..00000000000 --- a/src/black_primer/primer.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "configuration_format_version": 20210815, - "projects": { - "STDIN": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, - "git_clone_url": "", - "long_checkout": false, - "py_versions": ["all"] - }, - "aioexabgp": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, - "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "attrs": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/python-attrs/attrs.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "bandersnatch": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/pypa/bandersnatch.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "channels": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/django/channels.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "cpython": { - "disabled": true, - "disabled_reason": "To big / slow for GitHub Actions but handy to keep config to use manually or in some other CI in the future", - "base_path": "Lib", - "cli_arguments": [ - "--experimental-string-processing", - "--extend-exclude", - [ - "Lib/lib2to3/tests/data/different_encoding.py", - "|Lib/lib2to3/tests/data/false_encoding.py", - "|Lib/lib2to3/tests/data/py2_test_grammar.py", - "|Lib/test/bad_coding.py", - "|Lib/test/bad_coding2.py", - "|Lib/test/badsyntax_3131.py", - "|Lib/test/badsyntax_pep3120.py", - "|Lib/test/test_base64.py", - "|Lib/test/test_exceptions.py", - "|Lib/test/test_grammar.py", - "|Lib/test/test_named_expressions.py", - "|Lib/test/test_patma.py", - "|Lib/test/test_tokenize.py", - "|Lib/test/test_xml_etree.py", - "|Lib/traceback.py" - ] - ], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/python/cpython.git", - "long_checkout": false, - "py_versions": ["3.9", "3.10"], - "timeout_seconds": 900 - }, - "django": { - "cli_arguments": [ - "--experimental-string-processing", - "--skip-string-normalization", - "--extend-exclude", - "/((docs|scripts)/|django/forms/models.py|tests/gis_tests/test_spatialrefsys.py|tests/test_runner_apps/tagged/tests_syntax_error.py)" - ], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/django/django.git", - "long_checkout": false, - "py_versions": ["3.8", "3.9", "3.10"] - }, - "flake8-bugbear": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "hypothesis": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", - "long_checkout": false, - "py_versions": ["3.8", "3.9", "3.10"] - }, - "pandas": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/pandas-dev/pandas.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "pillow": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/python-pillow/Pillow.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "poetry": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, - "git_clone_url": "https://github.com/python-poetry/poetry.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "pyanalyze": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, - "git_clone_url": "https://github.com/quora/pyanalyze.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "pyramid": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/Pylons/pyramid.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "ptr": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": false, - "git_clone_url": "https://github.com/facebookincubator/ptr.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "pytest": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/pytest-dev/pytest.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "scikit-lego": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/koaning/scikit-lego", - "long_checkout": false, - "py_versions": ["all"] - }, - "tox": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/tox-dev/tox.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "typeshed": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/python/typeshed.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "virtualenv": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/pypa/virtualenv.git", - "long_checkout": false, - "py_versions": ["all"] - }, - "warehouse": { - "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/pypa/warehouse.git", - "long_checkout": false, - "py_versions": ["all"] - } - } -} diff --git a/tests/test_format.py b/tests/test_format.py index 04eda43d5cf..269bbacd249 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -103,8 +103,6 @@ "src/black/strings.py", "src/black/trans.py", "src/blackd/__init__.py", - "src/black_primer/cli.py", - "src/black_primer/lib.py", "src/blib2to3/pygram.py", "src/blib2to3/pytree.py", "src/blib2to3/pgen2/conv.py", @@ -119,7 +117,6 @@ "tests/test_black.py", "tests/test_blackd.py", "tests/test_format.py", - "tests/test_primer.py", "tests/optional.py", "tests/util.py", "tests/conftest.py", diff --git a/tests/test_primer.py b/tests/test_primer.py deleted file mode 100644 index 0a9d2aec495..00000000000 --- a/tests/test_primer.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import sys -import unittest -from contextlib import contextmanager -from copy import deepcopy -from io import StringIO -from os import getpid -from pathlib import Path -from platform import system -from pytest import LogCaptureFixture -from subprocess import CalledProcessError -from tempfile import TemporaryDirectory, gettempdir -from typing import Any, Callable, Iterator, List, Tuple, TypeVar -from unittest.mock import Mock, patch - -from click.testing import CliRunner - -from black_primer import cli, lib - - -EXPECTED_ANALYSIS_OUTPUT = """\ - -Failed projects: - -## black: - - Returned 69 - - stdout: -Black didn't work - --- primer results 📊 -- - -68 / 69 succeeded (98.55%) ✅ -1 / 69 FAILED (1.45%) 💩 - - 0 projects disabled by config - - 0 projects skipped due to Python version - - 0 skipped due to long checkout - -Failed projects: black - -""" -FAKE_PROJECT_CONFIG = { - "cli_arguments": ["--unittest"], - "expect_formatting_changes": False, - "git_clone_url": "https://github.com/psf/black.git", -} - - -@contextmanager -def capture_stdout( - command: Callable[..., Any], *args: Any, **kwargs: Any -) -> Iterator[str]: - old_stdout, sys.stdout = sys.stdout, StringIO() - try: - command(*args, **kwargs) - sys.stdout.seek(0) - yield sys.stdout.read() - finally: - sys.stdout = old_stdout - - -@contextmanager -def event_loop() -> Iterator[None]: - policy = asyncio.get_event_loop_policy() - loop = policy.new_event_loop() - asyncio.set_event_loop(loop) - if sys.platform == "win32": - asyncio.set_event_loop(asyncio.ProactorEventLoop()) - try: - yield - finally: - loop.close() - - -async def raise_subprocess_error_1(*args: Any, **kwargs: Any) -> None: - raise CalledProcessError(1, ["unittest", "error"], b"", b"") - - -async def raise_subprocess_error_123(*args: Any, **kwargs: Any) -> None: - raise CalledProcessError(123, ["unittest", "error"], b"", b"") - - -async def return_false(*args: Any, **kwargs: Any) -> bool: - return False - - -async def return_subproccess_output(*args: Any, **kwargs: Any) -> Tuple[bytes, bytes]: - return (b"stdout", b"stderr") - - -async def return_zero(*args: Any, **kwargs: Any) -> int: - return 0 - - -if sys.version_info >= (3, 9): - T = TypeVar("T") - Q = asyncio.Queue[T] -else: - T = Any - Q = asyncio.Queue - - -def collect(queue: Q) -> List[T]: - ret = [] - while True: - try: - item = queue.get_nowait() - ret.append(item) - except asyncio.QueueEmpty: - return ret - - -class PrimerLibTests(unittest.TestCase): - def test_analyze_results(self) -> None: - fake_results = lib.Results( - { - "disabled": 0, - "failed": 1, - "skipped_long_checkout": 0, - "success": 68, - "wrong_py_ver": 0, - }, - {"black": CalledProcessError(69, ["black"], b"Black didn't work", b"")}, - ) - with capture_stdout(lib.analyze_results, 69, fake_results) as analyze_stdout: - self.assertEqual(EXPECTED_ANALYSIS_OUTPUT, analyze_stdout) - - @event_loop() - def test_black_run(self) -> None: - """Pretend to run Black to ensure we cater for all scenarios""" - loop = asyncio.get_event_loop() - project_name = "unittest" - repo_path = Path(gettempdir()) - project_config = deepcopy(FAKE_PROJECT_CONFIG) - results = lib.Results({"failed": 0, "success": 0}, {}) - - # Test a successful Black run - with patch("black_primer.lib._gen_check_output", return_subproccess_output): - loop.run_until_complete( - lib.black_run(project_name, repo_path, project_config, results) - ) - self.assertEqual(1, results.stats["success"]) - self.assertFalse(results.failed_projects) - - # Test a fail based on expecting formatting changes but not getting any - project_config["expect_formatting_changes"] = True - results = lib.Results({"failed": 0, "success": 0}, {}) - with patch("black_primer.lib._gen_check_output", return_subproccess_output): - loop.run_until_complete( - lib.black_run(project_name, repo_path, project_config, results) - ) - self.assertEqual(1, results.stats["failed"]) - self.assertTrue(results.failed_projects) - - # Test a fail based on returning 1 and not expecting formatting changes - project_config["expect_formatting_changes"] = False - results = lib.Results({"failed": 0, "success": 0}, {}) - with patch("black_primer.lib._gen_check_output", raise_subprocess_error_1): - loop.run_until_complete( - lib.black_run(project_name, repo_path, project_config, results) - ) - self.assertEqual(1, results.stats["failed"]) - self.assertTrue(results.failed_projects) - - # Test a formatting error based on returning 123 - with patch("black_primer.lib._gen_check_output", raise_subprocess_error_123): - loop.run_until_complete( - lib.black_run(project_name, repo_path, project_config, results) - ) - self.assertEqual(2, results.stats["failed"]) - - def test_flatten_cli_args(self) -> None: - fake_long_args = ["--arg", ["really/", "|long", "|regex", "|splitup"], "--done"] - expected = ["--arg", "really/|long|regex|splitup", "--done"] - self.assertEqual(expected, lib._flatten_cli_args(fake_long_args)) - - @event_loop() - def test_gen_check_output(self) -> None: - loop = asyncio.get_event_loop() - stdout, stderr = loop.run_until_complete( - lib._gen_check_output([lib.BLACK_BINARY, "--help"]) - ) - self.assertIn("The uncompromising code formatter", stdout.decode("utf8")) - self.assertEqual(None, stderr) - - # TODO: Add a test to see failure works on Windows - if lib.WINDOWS: - return - - false_bin = "/usr/bin/false" if system() == "Darwin" else "/bin/false" - with self.assertRaises(CalledProcessError): - loop.run_until_complete(lib._gen_check_output([false_bin])) - - with self.assertRaises(asyncio.TimeoutError): - loop.run_until_complete( - lib._gen_check_output(["/bin/sleep", "2"], timeout=0.1) - ) - - @event_loop() - def test_git_checkout_or_rebase(self) -> None: - loop = asyncio.get_event_loop() - project_config = deepcopy(FAKE_PROJECT_CONFIG) - work_path = Path(gettempdir()) - - expected_repo_path = work_path / "black" - with patch("black_primer.lib._gen_check_output", return_subproccess_output): - returned_repo_path = loop.run_until_complete( - lib.git_checkout_or_rebase(work_path, project_config) - ) - self.assertEqual(expected_repo_path, returned_repo_path) - - @patch("sys.stdout", new_callable=StringIO) - @event_loop() - def test_process_queue(self, mock_stdout: Mock) -> None: - """Test the process queue on primer itself - - If you have non black conforming formatting in primer itself this can fail""" - loop = asyncio.get_event_loop() - config_path = Path(lib.__file__).parent / "primer.json" - with patch("black_primer.lib.git_checkout_or_rebase", return_false): - with TemporaryDirectory() as td: - return_val = loop.run_until_complete( - lib.process_queue( - str(config_path), Path(td), 2, ["django", "pyramid"] - ) - ) - self.assertEqual(0, return_val) - - @event_loop() - def test_load_projects_queue(self) -> None: - """Test the process queue on primer itself - - If you have non black conforming formatting in primer itself this can fail""" - loop = asyncio.get_event_loop() - config_path = Path(lib.__file__).parent / "primer.json" - - config, projects_queue = loop.run_until_complete( - lib.load_projects_queue(config_path, ["django", "pyramid"]) - ) - projects = collect(projects_queue) - self.assertEqual(projects, ["django", "pyramid"]) - - -class PrimerCLITests(unittest.TestCase): - @event_loop() - def test_async_main(self) -> None: - loop = asyncio.get_event_loop() - work_dir = Path(gettempdir()) / f"primer_ut_{getpid()}" - args = { - "config": "/config", - "debug": False, - "keep": False, - "long_checkouts": False, - "rebase": False, - "workdir": str(work_dir), - "workers": 69, - "no_diff": False, - "projects": "", - } - with patch("black_primer.cli.lib.process_queue", return_zero): - return_val = loop.run_until_complete(cli.async_main(**args)) # type: ignore - self.assertEqual(0, return_val) - - def test_handle_debug(self) -> None: - self.assertTrue(cli._handle_debug(None, None, True)) - - def test_help_output(self) -> None: - runner = CliRunner() - result = runner.invoke(cli.main, ["--help"]) - self.assertEqual(result.exit_code, 0) - - -def test_projects(caplog: LogCaptureFixture) -> None: - with event_loop(): - runner = CliRunner() - result = runner.invoke(cli.main, ["--projects=STDIN,asdf"]) - assert result.exit_code == 0 - assert "1 / 1 succeeded" in result.output - assert "Projects not found: {'asdf'}" in caplog.text - - caplog.clear() - - with event_loop(): - runner = CliRunner() - result = runner.invoke(cli.main, ["--projects=fdsa,STDIN"]) - assert result.exit_code == 0 - assert "1 / 1 succeeded" in result.output - assert "Projects not found: {'fdsa'}" in caplog.text - - -if __name__ == "__main__": - unittest.main() From 086ae68076de570b0cb1881a3c3b9da592b46ee0 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Wed, 16 Mar 2022 08:43:56 +0530 Subject: [PATCH 191/700] Remove power hugging formatting from preview (#2928) It is falsely placed in preview features and always formats the power operators, it was added in #2789 but there is no check for formatting added along with it. --- src/black/mode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/black/mode.py b/src/black/mode.py index 455ed36e27e..35a072c23e0 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -127,7 +127,6 @@ class Preview(Enum): """Individual preview style features.""" string_processing = auto() - hug_simple_powers = auto() class Deprecated(UserWarning): @@ -163,7 +162,9 @@ def __contains__(self, feature: Preview) -> bool: """ if feature is Preview.string_processing: return self.preview or self.experimental_string_processing - return self.preview + # TODO: Remove type ignore comment once preview contains more features + # than just ESP + return self.preview # type: ignore def get_cache_key(self) -> str: if self.target_versions: From fa7f01592b02229ff47f3bcab39a9b7d6c59f07c Mon Sep 17 00:00:00 2001 From: Joseph Young <80432516+jpy-git@users.noreply.github.com> Date: Wed, 16 Mar 2022 17:00:30 +0000 Subject: [PATCH 192/700] Update pylint config docs (#2931) --- CHANGES.md | 2 ++ docs/compatible_configs/pylint/pylintrc | 3 -- docs/compatible_configs/pylint/pyproject.toml | 3 -- docs/compatible_configs/pylint/setup.cfg | 3 -- docs/guides/using_black_with_other_tools.md | 32 +++---------------- 5 files changed, 6 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index da51e94342c..bb3ccb9ed9f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,8 @@ +- Update pylint config documentation (#2931) + ### Integrations diff --git a/docs/compatible_configs/pylint/pylintrc b/docs/compatible_configs/pylint/pylintrc index 7abddd2c330..e863488dfbc 100644 --- a/docs/compatible_configs/pylint/pylintrc +++ b/docs/compatible_configs/pylint/pylintrc @@ -1,5 +1,2 @@ -[MESSAGES CONTROL] -disable = C0330, C0326 - [format] max-line-length = 88 diff --git a/docs/compatible_configs/pylint/pyproject.toml b/docs/compatible_configs/pylint/pyproject.toml index 49ad7a2c771..ef51f98a966 100644 --- a/docs/compatible_configs/pylint/pyproject.toml +++ b/docs/compatible_configs/pylint/pyproject.toml @@ -1,5 +1,2 @@ -[tool.pylint.messages_control] -disable = "C0330, C0326" - [tool.pylint.format] max-line-length = "88" diff --git a/docs/compatible_configs/pylint/setup.cfg b/docs/compatible_configs/pylint/setup.cfg index 3ada24530ea..0b754cdc0f0 100644 --- a/docs/compatible_configs/pylint/setup.cfg +++ b/docs/compatible_configs/pylint/setup.cfg @@ -1,5 +1,2 @@ [pylint] max-line-length = 88 - -[pylint.messages_control] -disable = C0330, C0326 diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index bde99f7c00c..1d380bdaba7 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -210,31 +210,16 @@ mixed feelings about _Black_'s formatting style. #### Configuration ``` -disable = C0326, C0330 max-line-length = 88 ``` #### Why those options above? -When _Black_ is folding very long expressions, the closing brackets will -[be dedented](../the_black_code_style/current_style.md#how-black-wraps-lines). +Pylint should be configured to only complain about lines that surpass `88` characters +via `max-line-length = 88`. -```py3 -ImportantClass.important_method( - exc, limit, lookup_lines, capture_locals, callback -) -``` - -Although this style is PEP 8 compliant, Pylint will raise -`C0330: Wrong hanging indentation before block (add 4 spaces)` warnings. Since _Black_ -isn't configurable on this style, Pylint should be told to ignore these warnings via -`disable = C0330`. - -Also, since _Black_ deals with whitespace around operators and brackets, Pylint's -warning `C0326: Bad whitespace` should be disabled using `disable = C0326`. - -And as usual, Pylint should be configured to only complain about lines that surpass `88` -characters via `max-line-length = 88`. +If using `pylint<2.6.0`, also disable `C0326` and `C0330` as these are incompatible with +_Black_ formatting and have since been removed. #### Formats @@ -242,9 +227,6 @@ characters via `max-line-length = 88`. pylintrc ```ini -[MESSAGES CONTROL] -disable = C0326, C0330 - [format] max-line-length = 88 ``` @@ -257,9 +239,6 @@ max-line-length = 88 ```cfg [pylint] max-line-length = 88 - -[pylint.messages_control] -disable = C0326, C0330 ```
@@ -268,9 +247,6 @@ disable = C0326, C0330 pyproject.toml ```toml -[tool.pylint.messages_control] -disable = "C0326, C0330" - [tool.pylint.format] max-line-length = "88" ``` From f87df0e3c8735de416b6392ce7f21c6ba194424d Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Mon, 21 Mar 2022 21:51:07 +0000 Subject: [PATCH 193/700] dont skip formatting #%% (#2919) Fixes #2588 --- CHANGES.md | 2 ++ src/black/__init__.py | 2 +- src/black/comments.py | 48 ++++++++++++++++++++++++----------------- src/black/linegen.py | 16 ++++++++------ tests/data/comments8.py | 15 +++++++++++++ tests/test_format.py | 1 + 6 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 tests/data/comments8.py diff --git a/CHANGES.md b/CHANGES.md index bb3ccb9ed9f..d0faf7cecb9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ +- Code cell separators `#%%` are now standardised to `# %%` (#2919) + ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index c4ec99b441f..51e31e9598b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1166,7 +1166,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: else: versions = detect_target_versions(src_node, future_imports=future_imports) - normalize_fmt_off(src_node) + normalize_fmt_off(src_node, preview=mode.preview) lines = LineGenerator(mode=mode) elt = EmptyLineTracker(is_pyi=mode.is_pyi) empty_line = Line(mode=mode) diff --git a/src/black/comments.py b/src/black/comments.py index 28b9117101d..455326469f0 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -23,6 +23,8 @@ FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} +COMMENT_EXCEPTIONS = {True: " !:#'", False: " !:#'%"} + @dataclass class ProtoComment: @@ -42,7 +44,7 @@ class ProtoComment: consumed: int # how many characters of the original leaf's prefix did we consume -def generate_comments(leaf: LN) -> Iterator[Leaf]: +def generate_comments(leaf: LN, *, preview: bool) -> Iterator[Leaf]: """Clean the prefix of the `leaf` and generate comments from it, if any. Comments in lib2to3 are shoved into the whitespace prefix. This happens @@ -61,12 +63,16 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]: Inline comments are emitted as regular token.COMMENT leaves. Standalone are emitted with a fake STANDALONE_COMMENT token identifier. """ - for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER): + for pc in list_comments( + leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, preview=preview + ): yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) @lru_cache(maxsize=4096) -def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: +def list_comments( + prefix: str, *, is_endmarker: bool, preview: bool +) -> List[ProtoComment]: """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`.""" result: List[ProtoComment] = [] if not prefix or "#" not in prefix: @@ -92,7 +98,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: comment_type = token.COMMENT # simple trailing comment else: comment_type = STANDALONE_COMMENT - comment = make_comment(line) + comment = make_comment(line, preview=preview) result.append( ProtoComment( type=comment_type, value=comment, newlines=nlines, consumed=consumed @@ -102,10 +108,10 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: return result -def make_comment(content: str) -> str: +def make_comment(content: str, *, preview: bool) -> str: """Return a consistently formatted comment from the given `content` string. - All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single + All comments (except for "##", "#!", "#:", '#'") should have a single space between the hash sign and the content. If `content` didn't start with a hash sign, one is provided. @@ -123,26 +129,26 @@ def make_comment(content: str) -> str: and not content.lstrip().startswith("type:") ): content = " " + content[1:] # Replace NBSP by a simple space - if content and content[0] not in " !:#'%": + if content and content[0] not in COMMENT_EXCEPTIONS[preview]: content = " " + content return "#" + content -def normalize_fmt_off(node: Node) -> None: +def normalize_fmt_off(node: Node, *, preview: bool) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node) + try_again = convert_one_fmt_off_pair(node, preview=preview) -def convert_one_fmt_off_pair(node: Node) -> bool: +def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. """ for leaf in node.leaves(): previous_consumed = 0 - for comment in list_comments(leaf.prefix, is_endmarker=False): + for comment in list_comments(leaf.prefix, is_endmarker=False, preview=preview): if comment.value not in FMT_PASS: previous_consumed = comment.consumed continue @@ -157,7 +163,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: if comment.value in FMT_SKIP and prev.type in WHITESPACE: continue - ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + ignored_nodes = list(generate_ignored_nodes(leaf, comment, preview=preview)) if not ignored_nodes: continue @@ -197,7 +203,9 @@ def convert_one_fmt_off_pair(node: Node) -> bool: return False -def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: +def generate_ignored_nodes( + leaf: Leaf, comment: ProtoComment, *, preview: bool +) -> Iterator[LN]: """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. @@ -221,13 +229,13 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: yield leaf.parent return while container is not None and container.type != token.ENDMARKER: - if is_fmt_on(container): + if is_fmt_on(container, preview=preview): return # fix for fmt: on in children - if contains_fmt_on_at_column(container, leaf.column): + if contains_fmt_on_at_column(container, leaf.column, preview=preview): for child in container.children: - if contains_fmt_on_at_column(child, leaf.column): + if contains_fmt_on_at_column(child, leaf.column, preview=preview): return yield child else: @@ -235,12 +243,12 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: container = container.next_sibling -def is_fmt_on(container: LN) -> bool: +def is_fmt_on(container: LN, preview: bool) -> bool: """Determine whether formatting is switched on within a container. Determined by whether the last `# fmt:` comment is `on` or `off`. """ fmt_on = False - for comment in list_comments(container.prefix, is_endmarker=False): + for comment in list_comments(container.prefix, is_endmarker=False, preview=preview): if comment.value in FMT_ON: fmt_on = True elif comment.value in FMT_OFF: @@ -248,7 +256,7 @@ def is_fmt_on(container: LN) -> bool: return fmt_on -def contains_fmt_on_at_column(container: LN, column: int) -> bool: +def contains_fmt_on_at_column(container: LN, column: int, *, preview: bool) -> bool: """Determine if children at a given column have formatting switched on.""" for child in container.children: if ( @@ -257,7 +265,7 @@ def contains_fmt_on_at_column(container: LN, column: int) -> bool: or isinstance(child, Leaf) and child.column == column ): - if is_fmt_on(child): + if is_fmt_on(child, preview=preview): return True return False diff --git a/src/black/linegen.py b/src/black/linegen.py index 4dc242a1dfe..79475a83f0e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -72,7 +72,7 @@ def visit_default(self, node: LN) -> Iterator[Line]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Leaf): any_open_brackets = self.current_line.bracket_tracker.any_open_brackets() - for comment in generate_comments(node): + for comment in generate_comments(node, preview=self.mode.preview): if any_open_brackets: # any comment within brackets is subject to splitting self.current_line.append(comment) @@ -132,7 +132,7 @@ def visit_stmt( `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens) + normalize_invisible_parens(node, parens_after=parens, preview=self.mode.preview) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() @@ -141,7 +141,7 @@ def visit_stmt( def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set()) + normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) yield from self.line() for child in node.children: @@ -802,7 +802,9 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: leaf.prefix = "" -def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: +def normalize_invisible_parens( + node: Node, parens_after: Set[str], *, preview: bool +) -> None: """Make existing optional parentheses invisible or create new ones. `parens_after` is a set of string leaf values immediately after which parens @@ -811,7 +813,7 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. """ - for pc in list_comments(node.prefix, is_endmarker=False): + for pc in list_comments(node.prefix, is_endmarker=False, preview=preview): if pc.value in FMT_OFF: # This `node` has a prefix with `# fmt: off`, don't mess with parens. return @@ -820,7 +822,9 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: # Fixes a bug where invisible parens are not properly stripped from # assignment statements that contain type annotations. if isinstance(child, Node) and child.type == syms.annassign: - normalize_invisible_parens(child, parens_after=parens_after) + normalize_invisible_parens( + child, parens_after=parens_after, preview=preview + ) # Add parentheses around long tuple unpacking in assignments. if ( diff --git a/tests/data/comments8.py b/tests/data/comments8.py new file mode 100644 index 00000000000..a2030c2a092 --- /dev/null +++ b/tests/data/comments8.py @@ -0,0 +1,15 @@ +# The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. + +#%% +# %% + +# output + +# The percent-percent comments are Spyder IDE cells. +# Both `#%%`` and `# %%` are accepted, so `black` standardises +# to the latter. + +# %% +# %% diff --git a/tests/test_format.py b/tests/test_format.py index 269bbacd249..667d5c110fa 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -75,6 +75,7 @@ # string processing "cantfit", "comments7", + "comments8", "long_strings", "long_strings__edge_case", "long_strings__regression", From 5379d4f3f460ec9b7063dd1cc10f437b0edf9ae3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 21 Mar 2022 15:20:41 -0700 Subject: [PATCH 194/700] stub style: remove some possible future changes (#2940) Fixes #2938. All of these suggested future changes are out of scope for an autoformatter and should be handled by a linter instead. --- docs/the_black_code_style/current_style.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 0bf5894abdd..d54c7abaf5d 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -399,16 +399,11 @@ recommended code style for those files is more terse than PEP 8: _Black_ enforces the above rules. There are additional guidelines for formatting `.pyi` file that are not enforced yet but might be in a future version of the formatter: -- all function bodies should be empty (contain `...` instead of the body); -- do not use docstrings; - prefer `...` over `pass`; -- for arguments with a default, use `...` instead of the actual default; - avoid using string literals in type annotations, stub files support forward references natively (like Python 3.7 code with `from __future__ import annotations`); - use variable annotations instead of type comments, even for stubs that target older - versions of Python; -- for arguments that default to `None`, use `Optional[]` explicitly; -- use `float` instead of `Union[int, float]`. + versions of Python. ## Pragmatism From 062b54931dc3ea35f673e755893fe28ff1f5a889 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 23 Mar 2022 13:31:13 -0400 Subject: [PATCH 195/700] Github now supports .git-blame-ignore-revs (GH-2948) It's in beta. https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view --- docs/guides/introducing_black_to_your_project.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/guides/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md index 71ccf7c114c..9ae40a1928e 100644 --- a/docs/guides/introducing_black_to_your_project.md +++ b/docs/guides/introducing_black_to_your_project.md @@ -43,8 +43,10 @@ call to `git blame`. $ git config blame.ignoreRevsFile .git-blame-ignore-revs ``` -**The one caveat is that GitHub and GitLab do not yet support ignoring revisions using -their native UI of blame.** So blame information will be cluttered with a reformatting -commit on those platforms. (If you'd like this feature, there's an open issue for -[GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423) and please let GitHub -know!) +**The one caveat is that some online Git-repositories like GitLab do not yet support +ignoring revisions using their native blame UI.** So blame information will be cluttered +with a reformatting commit on those platforms. (If you'd like this feature, there's an +open issue for [GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423)). This is +however supported by +[GitHub](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view), +currently in beta. From 3800ebd81df6a1c31d1eac8cc15899537b9cbb61 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Thu, 24 Mar 2022 02:16:09 +0000 Subject: [PATCH 196/700] Avoid magic-trailing-comma in single-element subscripts (#2942) Closes #2918. --- CHANGES.md | 1 + src/black/linegen.py | 11 ++++++--- src/black/lines.py | 21 ++++++++++++++--- src/black/mode.py | 5 ++-- src/black/nodes.py | 12 +++++++--- tests/data/one_element_subscript.py | 36 +++++++++++++++++++++++++++++ tests/test_format.py | 1 + 7 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 tests/data/one_element_subscript.py diff --git a/CHANGES.md b/CHANGES.md index d0faf7cecb9..d753a24ff77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Code cell separators `#%%` are now standardised to `# %%` (#2919) +- Avoid magic-trailing-comma in single-element subscripts (#2942) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 79475a83f0e..5d92011da9a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -8,7 +8,12 @@ from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS 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_docstring, + is_empty_tuple, + is_one_tuple, + is_one_sequence_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 from black.nodes import is_stub_suite, is_stub_body, is_atom_with_invisible_parens @@ -973,7 +978,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf prev and prev.type == token.COMMA and leaf.opening_bracket is not None - and not is_one_tuple_between( + and not is_one_sequence_between( leaf.opening_bracket, leaf, line.leaves ) ): @@ -1001,7 +1006,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf 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) + and not is_one_sequence_between(leaf.opening_bracket, leaf, line.leaves) ): # Never omit bracket pairs with trailing commas. # We need to explode on those. diff --git a/src/black/lines.py b/src/black/lines.py index f35665c8e0c..e455a507539 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -17,12 +17,12 @@ from blib2to3.pgen2 import token from black.brackets import BracketTracker, DOT_PRIORITY -from black.mode import Mode +from black.mode import Mode, Preview 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 -from black.nodes import is_one_tuple_between +from black.nodes import is_one_sequence_between # types T = TypeVar("T") @@ -254,6 +254,7 @@ def has_magic_trailing_comma( """Return True if we have a magic trailing comma, that is when: - there's a trailing comma here - it's not a one-tuple + - it's not a single-element subscript Additionally, if ensure_removable: - it's not from square bracket indexing """ @@ -268,6 +269,20 @@ def has_magic_trailing_comma( return True if closing.type == token.RSQB: + if ( + Preview.one_element_subscript in self.mode + and closing.parent + and closing.parent.type == syms.trailer + and closing.opening_bracket + and is_one_sequence_between( + closing.opening_bracket, + closing, + self.leaves, + brackets=(token.LSQB, token.RSQB), + ) + ): + return False + if not ensure_removable: return True comma = self.leaves[-1] @@ -276,7 +291,7 @@ def has_magic_trailing_comma( if self.is_import: return True - if closing.opening_bracket is not None and not is_one_tuple_between( + if closing.opening_bracket is not None and not is_one_sequence_between( closing.opening_bracket, closing, self.leaves ): return True diff --git a/src/black/mode.py b/src/black/mode.py index 35a072c23e0..77b1cabfcbc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -127,6 +127,7 @@ class Preview(Enum): """Individual preview style features.""" string_processing = auto() + one_element_subscript = auto() class Deprecated(UserWarning): @@ -162,9 +163,7 @@ def __contains__(self, feature: Preview) -> bool: """ if feature is Preview.string_processing: return self.preview or self.experimental_string_processing - # TODO: Remove type ignore comment once preview contains more features - # than just ESP - return self.preview # type: ignore + return self.preview def get_cache_key(self) -> str: if self.target_versions: diff --git a/src/black/nodes.py b/src/black/nodes.py index f130bff990e..d18d4bde872 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -9,6 +9,7 @@ List, Optional, Set, + Tuple, TypeVar, Union, ) @@ -559,9 +560,14 @@ def is_one_tuple(node: LN) -> bool: ) -def is_one_tuple_between(opening: Leaf, closing: Leaf, leaves: List[Leaf]) -> bool: - """Return True if content between `opening` and `closing` looks like a one-tuple.""" - if opening.type != token.LPAR and closing.type != token.RPAR: +def is_one_sequence_between( + opening: Leaf, + closing: Leaf, + leaves: List[Leaf], + brackets: Tuple[int, int] = (token.LPAR, token.RPAR), +) -> bool: + """Return True if content between `opening` and `closing` is a one-sequence.""" + if (opening.type, closing.type) != brackets: return False depth = closing.bracket_depth + 1 diff --git a/tests/data/one_element_subscript.py b/tests/data/one_element_subscript.py new file mode 100644 index 00000000000..39205ba9f7a --- /dev/null +++ b/tests/data/one_element_subscript.py @@ -0,0 +1,36 @@ +# We should not treat the trailing comma +# in a single-element subscript. +a: tuple[int,] +b = tuple[int,] + +# The magic comma still applies to multi-element subscripts. +c: tuple[int, int,] +d = tuple[int, int,] + +# Magic commas still work as expected for non-subscripts. +small_list = [1,] +list_of_types = [tuple[int,],] + +# output +# We should not treat the trailing comma +# in a single-element subscript. +a: tuple[int,] +b = tuple[int,] + +# The magic comma still applies to multi-element subscripts. +c: tuple[ + int, + int, +] +d = tuple[ + int, + int, +] + +# Magic commas still work as expected for non-subscripts. +small_list = [ + 1, +] +list_of_types = [ + tuple[int,], +] diff --git a/tests/test_format.py b/tests/test_format.py index 667d5c110fa..4de31700268 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -80,6 +80,7 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", + "one_element_subscript", ] SOURCES: List[str] = [ From 14e5ce5412efa53438df0180e735b3834df3b579 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Thu, 24 Mar 2022 14:59:54 +0000 Subject: [PATCH 197/700] Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) --- CHANGES.md | 1 + src/black/linegen.py | 26 ++++++++++++++--- src/black/mode.py | 1 + src/black/trans.py | 12 ++++---- tests/data/long_strings__regression.py | 2 +- tests/data/remove_for_brackets.py | 40 ++++++++++++++++++++++++++ tests/test_format.py | 1 + 7 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 tests/data/remove_for_brackets.py diff --git a/CHANGES.md b/CHANGES.md index d753a24ff77..b2325b648f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Code cell separators `#%%` are now standardised to `# %%` (#2919) +- Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) - Avoid magic-trailing-comma in single-element subscripts (#2942) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 5d92011da9a..cb605ee8be4 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -841,7 +841,11 @@ def normalize_invisible_parens( if check_lpar: if child.type == syms.atom: - if maybe_make_parens_invisible_in_atom(child, parent=node): + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + preview=preview, + ): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) @@ -865,7 +869,11 @@ def normalize_invisible_parens( check_lpar = isinstance(child, Leaf) and child.value in parens_after -def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: +def maybe_make_parens_invisible_in_atom( + node: LN, + parent: LN, + preview: bool = False, +) -> bool: """If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` as they are redundant. @@ -873,13 +881,23 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: Returns whether the node should itself be wrapped in invisible parentheses. """ + if ( + preview + and parent.type == syms.for_stmt + and isinstance(node.prev_sibling, Leaf) + and node.prev_sibling.type == token.NAME + and node.prev_sibling.value == "for" + ): + for_stmt_check = False + else: + for_stmt_check = True if ( node.type != syms.atom or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and for_stmt_check) ): return False @@ -902,7 +920,7 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: # make parentheses invisible first.value = "" last.value = "" - maybe_make_parens_invisible_in_atom(middle, parent=parent) + maybe_make_parens_invisible_in_atom(middle, parent=parent, preview=preview) if is_atom_with_invisible_parens(middle): # Strip the invisible parens from `middle` by replacing diff --git a/src/black/mode.py b/src/black/mode.py index 77b1cabfcbc..6b74c14b6de 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -127,6 +127,7 @@ class Preview(Enum): """Individual preview style features.""" string_processing = auto() + remove_redundant_parens = auto() one_element_subscript = auto() diff --git a/src/black/trans.py b/src/black/trans.py index 74d052fe2dc..01aa80eaaf8 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -365,7 +365,7 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for (i, leaf) in enumerate(LL): + for i, leaf in enumerate(LL): if ( leaf.type == token.STRING and is_valid_index(i + 1) @@ -570,7 +570,7 @@ def make_naked(string: str, string_prefix: str) -> str: # Build the final line ('new_line') that this method will later return. new_line = line.clone() - for (i, leaf) in enumerate(LL): + for i, leaf in enumerate(LL): if i == string_idx: new_line.append(string_leaf) @@ -691,7 +691,7 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for (idx, leaf) in enumerate(LL): + for idx, leaf in enumerate(LL): # Should be a string... if leaf.type != token.STRING: continue @@ -1713,7 +1713,7 @@ def _assert_match(LL: List[Leaf]) -> Optional[int]: if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert": is_valid_index = is_valid_index_factory(LL) - for (i, leaf) in enumerate(LL): + for i, leaf in enumerate(LL): # We MUST find a comma... if leaf.type == token.COMMA: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 @@ -1751,7 +1751,7 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: ): is_valid_index = is_valid_index_factory(LL) - for (i, leaf) in enumerate(LL): + for i, leaf in enumerate(LL): # We MUST find either an '=' or '+=' symbol... if leaf.type in [token.EQUAL, token.PLUSEQUAL]: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 @@ -1794,7 +1794,7 @@ def _dict_match(LL: List[Leaf]) -> Optional[int]: if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]: is_valid_index = is_valid_index_factory(LL) - for (i, leaf) in enumerate(LL): + for i, leaf in enumerate(LL): # We MUST find a colon... if leaf.type == token.COLON: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 36f323e04d6..58ccc4ac0b1 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -599,7 +599,7 @@ def foo(): def foo(xxxx): - for (xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx) in xxxx: + for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx: for xxx in xxx_xxxx: assert ("x" in xxx) or (xxx in xxx_xxx_xxxxx), ( "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}" diff --git a/tests/data/remove_for_brackets.py b/tests/data/remove_for_brackets.py new file mode 100644 index 00000000000..c8d88abcc50 --- /dev/null +++ b/tests/data/remove_for_brackets.py @@ -0,0 +1,40 @@ +# Only remove tuple brackets after `for` +for (k, v) in d.items(): + print(k, v) + +# Don't touch tuple brackets after `in` +for module in (core, _unicodefun): + if hasattr(module, "_verify_python3_env"): + module._verify_python3_env = lambda: None + +# Brackets remain for long for loop lines +for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items(): + print(k, v) + +for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): + print(k, v) + +# output +# Only remove tuple brackets after `for` +for k, v in d.items(): + print(k, v) + +# Don't touch tuple brackets after `in` +for module in (core, _unicodefun): + if hasattr(module, "_verify_python3_env"): + module._verify_python3_env = lambda: None + +# Brackets remain for long for loop lines +for ( + why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, + i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, +) in d.items(): + print(k, v) + +for ( + k, + v, +) in ( + dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() +): + print(k, v) diff --git a/tests/test_format.py b/tests/test_format.py index 4de31700268..b7446853e7b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -80,6 +80,7 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", + "remove_for_brackets", "one_element_subscript", ] From 14d84ba2e96c5ca1351b8fe4d0d415cc148f4117 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:14:21 +0000 Subject: [PATCH 198/700] Resolve new flake8-bugbear errors (B020) (GH-2950) Fixes a couple places where we were using the same variable name as we are iterating over. Co-authored-by: Jelle Zijlstra --- src/black/linegen.py | 6 +++--- src/black/parsing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index cb605ee8be4..9c85e76d3e8 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -670,9 +670,9 @@ def dont_increase_indentation(split_func: Transformer) -> Transformer: @wraps(split_func) def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: - for line in split_func(line, features): - normalize_prefix(line.leaves[0], inside_brackets=True) - yield line + for split_line in split_func(line, features): + normalize_prefix(split_line.leaves[0], inside_brackets=True) + yield split_line return split_wrapper diff --git a/src/black/parsing.py b/src/black/parsing.py index db48ae4baf5..12726567948 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -225,8 +225,8 @@ def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[st and isinstance(node, (ast.Delete, ast3.Delete)) and isinstance(item, (ast.Tuple, ast3.Tuple)) ): - for item in item.elts: - yield from stringify_ast(item, depth + 2) + for elt in item.elts: + yield from stringify_ast(elt, depth + 2) elif isinstance(item, (ast.AST, ast3.AST)): yield from stringify_ast(item, depth + 2) From bd1e98034907463f5d86f4d87e89202dc6c34dd4 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Sat, 26 Mar 2022 16:56:50 +0000 Subject: [PATCH 199/700] Remove unnecessary parentheses from `except` clauses (#2939) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/linegen.py | 7 ++- tests/data/remove_except_parens.py | 79 ++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/data/remove_except_parens.py diff --git a/CHANGES.md b/CHANGES.md index b2325b648f9..d34bd4ead58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Code cell separators `#%%` are now standardised to `# %%` (#2919) +- Remove unnecessary parentheses from `except` statements (#2939) - Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) - Avoid magic-trailing-comma in single-element subscripts (#2942) diff --git a/src/black/linegen.py b/src/black/linegen.py index 9c85e76d3e8..8a28c3901bb 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -318,7 +318,12 @@ def __post_init__(self) -> None: self.visit_try_stmt = partial( v, keywords={"try", "except", "else", "finally"}, parens=Ø ) - self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) + if self.mode.preview: + self.visit_except_clause = partial( + v, keywords={"except"}, parens={"except"} + ) + else: + self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) diff --git a/tests/data/remove_except_parens.py b/tests/data/remove_except_parens.py new file mode 100644 index 00000000000..322c5b7a51b --- /dev/null +++ b/tests/data/remove_except_parens.py @@ -0,0 +1,79 @@ +# These brackets are redundant, therefore remove. +try: + a.something +except (AttributeError) as err: + raise err + +# This is tuple of exceptions. +# Although this could be replaced with just the exception, +# we do not remove brackets to preserve AST. +try: + a.something +except (AttributeError,) as err: + raise err + +# This is a tuple of exceptions. Do not remove brackets. +try: + a.something +except (AttributeError, ValueError) as err: + raise err + +# Test long variants. +try: + a.something +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: + raise err + +try: + a.something +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err: + raise err + +try: + a.something +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: + raise err + +# output +# These brackets are redundant, therefore remove. +try: + a.something +except AttributeError as err: + raise err + +# This is tuple of exceptions. +# Although this could be replaced with just the exception, +# we do not remove brackets to preserve AST. +try: + a.something +except (AttributeError,) as err: + raise err + +# This is a tuple of exceptions. Do not remove brackets. +try: + a.something +except (AttributeError, ValueError) as err: + raise err + +# Test long variants. +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error +) as err: + raise err + +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + raise err + +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + raise err diff --git a/tests/test_format.py b/tests/test_format.py index b7446853e7b..a995bd3f1f5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -80,6 +80,7 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", + "remove_except_parens", "remove_for_brackets", "one_element_subscript", ] From f239d227c003c52126239e1b9a37c36c2b2b8305 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 26 Mar 2022 17:22:38 -0400 Subject: [PATCH 200/700] Enforce no formatting changes for PRs via CI (GH-2951) Now PRs will run two diff-shades jobs, "preview-changes" which formats all projects with preview=True, and "assert-no-changes" which formats all projects with preview=False. The latter also fails if any changes were made. Pushes to main will only run "preview-changes" Also the workflow_dispatch feature was dropped since it was complicating everything for little gain. --- .github/workflows/diff_shades.yml | 114 ++++++++++++++------------- docs/contributing/gauging_changes.md | 50 ++++++------ scripts/diff_shades_gha_helper.py | 105 ++++++++---------------- src/black/__init__.py | 3 + 4 files changed, 121 insertions(+), 151 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index e9deaba0136..51fcebcff63 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -3,54 +3,61 @@ name: diff-shades on: push: branches: [main] - paths-ignore: ["docs/**", "tests/**", "**.md", "**.rst"] + paths: ["src/**", "setup.*", "pyproject.toml", ".github/workflows/*"] pull_request: - paths-ignore: ["docs/**", "tests/**", "**.md", "**.rst"] - - workflow_dispatch: - inputs: - baseline: - description: > - The baseline revision. Pro-tip, use `.pypi` to use the latest version - on PyPI or `.XXX` to use a PR. - required: true - default: main - baseline-args: - description: "Custom Black arguments (eg. -l 79)" - required: false - target: - description: > - The target revision to compare against the baseline. Same tip applies here. - required: true - target-args: - description: "Custom Black arguments (eg. -S)" - required: false + paths: ["src/**", "setup.*", "pyproject.toml", ".github/workflows/*"] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true jobs: + configure: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-config.outputs.matrix }} + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + + - name: Install diff-shades and support dependencies + run: | + python -m pip install click packaging urllib3 + python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip + + - name: Calculate run configuration & metadata + id: set-config + env: + GITHUB_TOKEN: ${{ github.token }} + run: > + python scripts/diff_shades_gha_helper.py config ${{ github.event_name }} ${{ matrix.mode }} + analysis: - name: analysis / linux + name: analysis / ${{ matrix.mode }} + needs: configure runs-on: ubuntu-latest env: # Clang is less picky with the C code it's given than gcc (and may # generate faster binaries too). CC: clang-12 + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.configure.outputs.matrix )}} steps: - name: Checkout this repository (full clone) uses: actions/checkout@v3 with: + # The baseline revision could be rather old so a full clone is ideal. fetch-depth: 0 - uses: actions/setup-python@v3 - name: Install diff-shades and support dependencies run: | - python -m pip install pip --upgrade python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip python -m pip install click packaging urllib3 python -m pip install -r .github/mypyc-requirements.txt @@ -59,28 +66,19 @@ jobs: git config user.name "diff-shades-gha" git config user.email "diff-shades-gha@example.com" - - name: Calculate run configuration & metadata - id: config - env: - GITHUB_TOKEN: ${{ github.token }} - run: > - python helper.py config ${{ github.event_name }} - ${{ github.event.inputs.baseline }} ${{ github.event.inputs.target }} - --baseline-args "${{ github.event.inputs.baseline-args }}" - - name: Attempt to use cached baseline analysis id: baseline-cache uses: actions/cache@v2.1.7 with: - path: ${{ steps.config.outputs.baseline-analysis }} - key: ${{ steps.config.outputs.baseline-cache-key }} + path: ${{ matrix.baseline-analysis }} + key: ${{ matrix.baseline-cache-key }} - name: Build and install baseline revision if: steps.baseline-cache.outputs.cache-hit != 'true' env: GITHUB_TOKEN: ${{ github.token }} run: > - ${{ steps.config.outputs.baseline-setup-cmd }} + ${{ matrix.baseline-setup-cmd }} && python setup.py --use-mypyc bdist_wheel && python -m pip install dist/*.whl && rm build dist -r @@ -88,63 +86,69 @@ jobs: if: steps.baseline-cache.outputs.cache-hit != 'true' run: > diff-shades analyze -v --work-dir projects-cache/ - ${{ steps.config.outputs.baseline-analysis }} -- ${{ github.event.inputs.baseline-args }} + ${{ matrix.baseline-analysis }} ${{ matrix.force-flag }} - name: Build and install target revision env: GITHUB_TOKEN: ${{ github.token }} run: > - ${{ steps.config.outputs.target-setup-cmd }} + ${{ matrix.target-setup-cmd }} && python setup.py --use-mypyc bdist_wheel && python -m pip install dist/*.whl - name: Analyze target revision run: > diff-shades analyze -v --work-dir projects-cache/ - ${{ steps.config.outputs.target-analysis }} --repeat-projects-from - ${{ steps.config.outputs.baseline-analysis }} -- ${{ github.event.inputs.target-args }} + ${{ matrix.target-analysis }} --repeat-projects-from + ${{ matrix.baseline-analysis }} ${{ matrix.force-flag }} - name: Generate HTML diff report run: > - diff-shades --dump-html diff.html compare --diff --quiet - ${{ steps.config.outputs.baseline-analysis }} ${{ steps.config.outputs.target-analysis }} + diff-shades --dump-html diff.html compare --diff + ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} - name: Upload diff report uses: actions/upload-artifact@v2 with: - name: diff.html + name: ${{ matrix.mode }}-diff.html path: diff.html - name: Upload baseline analysis uses: actions/upload-artifact@v2 with: - name: ${{ steps.config.outputs.baseline-analysis }} - path: ${{ steps.config.outputs.baseline-analysis }} + name: ${{ matrix.baseline-analysis }} + path: ${{ matrix.baseline-analysis }} - name: Upload target analysis uses: actions/upload-artifact@v2 with: - name: ${{ steps.config.outputs.target-analysis }} - path: ${{ steps.config.outputs.target-analysis }} + name: ${{ matrix.target-analysis }} + path: ${{ matrix.target-analysis }} - name: Generate summary file (PR only) - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes' run: > python helper.py comment-body - ${{ steps.config.outputs.baseline-analysis }} ${{ steps.config.outputs.target-analysis }} - ${{ steps.config.outputs.baseline-sha }} ${{ steps.config.outputs.target-sha }} + ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} + ${{ matrix.baseline-sha }} ${{ matrix.target-sha }} ${{ github.event.pull_request.number }} - name: Upload summary file (PR only) - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes' uses: actions/upload-artifact@v2 with: name: .pr-comment.json path: .pr-comment.json - # This is last so the diff-shades-comment workflow can still work even if we - # end up detecting failed files and failing the run. - - name: Check for failed files in both analyses + - name: Verify zero changes (PR only) + if: matrix.mode == 'assert-no-changes' + run: > + diff-shades compare --check ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} + || (echo "Please verify you didn't change the stable code style unintentionally!" && exit 1) + + - name: Check for failed files for target revision + # Even if the previous step failed, we should still check for failed files. + if: always() run: > - diff-shades show-failed --check --show-log ${{ steps.config.outputs.baseline-analysis }}; - diff-shades show-failed --check --show-log ${{ steps.config.outputs.target-analysis }} + diff-shades show-failed --check --show-log ${{ matrix.target-analysis }} + --check-allow 'sqlalchemy:test/orm/test_relationship_criteria.py' diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index f28e81120b3..8562a83ed0c 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -9,10 +9,10 @@ enough to cause frustration to projects that are already "black formatted". ## diff-shades -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. +diff-shades is a tool that runs _Black_ across a list of open-source 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]. @@ -20,35 +20,39 @@ For more information, please see the [diff-shades documentation][diff-shades]. diff-shades is also the tool behind the "diff-shades results comparing ..." / "diff-shades reports zero changes ..." comments on PRs. The project has a GitHub Actions -workflow which runs diff-shades twice against two revisions of _Black_ according to -these rules: +workflow that analyzes and compares two revisions of _Black_ according to these rules: | | Baseline revision | Target revision | | --------------------- | ----------------------- | ---------------------------- | | On PRs | latest commit on `main` | PR commit with `main` merged | | On pushes (main only) | latest PyPI version | the pushed commit | -Once finished, a PR comment will be posted embedding a summary of the changes and links -to further information. If there's a pre-existing diff-shades comment, it'll be updated -instead the next time the workflow is triggered on the same PR. +For pushes to main, there's only one analysis job named `preview-changes` where the +preview style is used for all projects. -The workflow uploads 3-4 artifacts upon completion: the two generated analyses (they -have the .json file extension), `diff.html`, and `.pr-comment.json` if triggered by a -PR. The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be -downloaded locally. `diff.html` comes in handy for push-based or manually triggered -runs. And the analyses exist just in case you want to do further analysis using the -collected data locally. +For PRs they get one more analysis job: `assert-no-changes`. It's similar to +`preview-changes` but runs with the stable code style. It will fail if changes were +made. This makes sure code won't be reformatted again and again within the same year in +accordance to Black's stability policy. -Note that the workflow will only fail intentionally if while analyzing a file failed to +Additionally for PRs, a PR comment will be posted embedding a summary of the preview +changes and links to further information. If there's a pre-existing diff-shades comment, +it'll be updated instead the next time the workflow is triggered on the same PR. + +```{note} +The `preview-changes` job will only fail intentionally if while analyzing a file failed to format. Otherwise a failure indicates a bug in the workflow. +``` -```{tip} -Maintainers with write access or higher can trigger the workflow manually from the -Actions tab using the `workflow_dispatch` event. Simply select "diff-shades" -from the workflows list on the left, press "Run workflow", and fill in which revisions -and command line arguments to use. +The workflow uploads several artifacts upon completion: -Once finished, check the logs or download the artifacts for local use. -``` +- The raw analyses (.json) +- HTML diffs (.html) +- `.pr-comment.json` (if triggered by a PR) + +The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be +downloaded locally. The HTML diffs come in handy for push-based where there's no PR to +post a comment. And the analyses exist just in case you want to do further analysis +using the collected data locally. [diff-shades]: https://github.com/ichard26/diff-shades#readme diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index f1f7f2be91c..b5fea5a817d 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -23,7 +23,7 @@ import zipfile from io import BytesIO from pathlib import Path -from typing import Any, Optional, Tuple +from typing import Any import click import urllib3 @@ -55,7 +55,7 @@ def set_output(name: str, value: str) -> None: print(f"::set-output name={name}::{value}") -def http_get(url: str, is_json: bool = True, **kwargs: Any) -> Any: +def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any: headers = kwargs.get("headers") or {} headers["User-Agent"] = USER_AGENT if "github" in url: @@ -78,10 +78,10 @@ def http_get(url: str, is_json: bool = True, **kwargs: Any) -> Any: return data -def get_branch_or_tag_revision(sha: str = "main") -> str: +def get_main_revision() -> str: data = http_get( f"https://api.github.com/repos/{REPO}/commits", - fields={"per_page": "1", "sha": sha}, + fields={"per_page": "1", "sha": "main"}, ) assert isinstance(data[0]["sha"], str) return data[0]["sha"] @@ -100,53 +100,18 @@ def get_pypi_version() -> Version: return sorted_versions[0] -def resolve_custom_ref(ref: str) -> Tuple[str, str]: - if ref == ".pypi": - # Special value to get latest PyPI version. - version = str(get_pypi_version()) - return version, f"git checkout {version}" - - if ref.startswith(".") and ref[1:].isnumeric(): - # Special format to get a PR. - number = int(ref[1:]) - revision = get_pr_revision(number) - return ( - f"pr-{number}-{revision[:SHA_LENGTH]}", - f"gh pr checkout {number} && git merge origin/main", - ) - - # Alright, it's probably a branch, tag, or a commit SHA, let's find out! - revision = get_branch_or_tag_revision(ref) - # We're cutting the revision short as we might be operating on a short commit SHA. - if revision == ref or revision[: len(ref)] == ref: - # It's *probably* a commit as the resolved SHA isn't different from the REF. - return revision[:SHA_LENGTH], f"git checkout {revision}" - - # It's *probably* a pre-existing branch or tag, yay! - return f"{ref}-{revision[:SHA_LENGTH]}", f"git checkout {revision}" - - @click.group() def main() -> None: pass @main.command("config", help="Acquire run configuration and metadata.") -@click.argument( - "event", type=click.Choice(["push", "pull_request", "workflow_dispatch"]) -) -@click.argument("custom_baseline", required=False) -@click.argument("custom_target", required=False) -@click.option("--baseline-args", default="") -def config( - event: Literal["push", "pull_request", "workflow_dispatch"], - custom_baseline: Optional[str], - custom_target: Optional[str], - baseline_args: str, -) -> None: +@click.argument("event", type=click.Choice(["push", "pull_request"])) +def config(event: Literal["push", "pull_request"]) -> None: import diff_shades if event == "push": + jobs = [{"mode": "preview-changes", "force-flag": "--force-preview-style"}] # Push on main, let's use PyPI Black as the baseline. baseline_name = str(get_pypi_version()) baseline_cmd = f"git checkout {baseline_name}" @@ -156,11 +121,14 @@ def config( target_cmd = f"git checkout {target_rev}" elif event == "pull_request": + jobs = [ + {"mode": "preview-changes", "force-flag": "--force-preview-style"}, + {"mode": "assert-no-changes", "force-flag": "--force-stable-style"}, + ] # PR, let's use main as the baseline. - baseline_rev = get_branch_or_tag_revision() + baseline_rev = get_main_revision() baseline_name = "main-" + baseline_rev[:SHA_LENGTH] baseline_cmd = f"git checkout {baseline_rev}" - pr_ref = os.getenv("GITHUB_REF") assert pr_ref is not None pr_num = int(pr_ref[10:-6]) @@ -168,27 +136,20 @@ def config( target_name = f"pr-{pr_num}-{pr_rev[:SHA_LENGTH]}" target_cmd = f"gh pr checkout {pr_num} && git merge origin/main" - # These are only needed for the PR comment. - set_output("baseline-sha", baseline_rev) - set_output("target-sha", pr_rev) - else: - assert custom_baseline is not None and custom_target is not None - baseline_name, baseline_cmd = resolve_custom_ref(custom_baseline) - target_name, target_cmd = resolve_custom_ref(custom_target) - if baseline_name == target_name: - # Alright we're using the same revisions but we're (hopefully) using - # different command line arguments, let's support that too. - baseline_name += "-1" - target_name += "-2" - - set_output("baseline-analysis", baseline_name + ".json") - set_output("baseline-setup-cmd", baseline_cmd) - set_output("target-analysis", target_name + ".json") - set_output("target-setup-cmd", target_cmd) + env = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}" + for entry in jobs: + entry["baseline-analysis"] = f"{entry['mode']}-{baseline_name}.json" + entry["baseline-setup-cmd"] = baseline_cmd + entry["target-analysis"] = f"{entry['mode']}-{target_name}.json" + entry["target-setup-cmd"] = target_cmd + entry["baseline-cache-key"] = f"{env}-{baseline_name}-{entry['mode']}" + if event == "pull_request": + # These are only needed for the PR comment. + entry["baseline-sha"] = baseline_rev + entry["target-sha"] = pr_rev - key = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}" - key += f"-{baseline_name}-{baseline_args.encode('utf-8').hex()}" - set_output("baseline-cache-key", key) + set_output("matrix", json.dumps(jobs, indent=None)) + pprint.pprint(jobs) @main.command("comment-body", help="Generate the body for a summary PR comment.") @@ -238,15 +199,13 @@ def comment_details(run_id: str) -> None: set_output("needs-comment", "true") jobs = http_get(data["jobs_url"])["jobs"] - assert len(jobs) == 1, "multiple jobs not supported nor tested" - job = jobs[0] - steps = {s["name"]: s["number"] for s in job["steps"]} - diff_step = steps[DIFF_STEP_NAME] - diff_url = job["html_url"] + f"#step:{diff_step}:1" - - artifacts_data = http_get(data["artifacts_url"])["artifacts"] - artifacts = {a["name"]: a["archive_download_url"] for a in artifacts_data} - comment_url = artifacts[COMMENT_FILE] + job = next(j for j in jobs if j["name"] == "analysis / preview-changes") + diff_step = next(s for s in job["steps"] if s["name"] == DIFF_STEP_NAME) + diff_url = job["html_url"] + f"#step:{diff_step['number']}:1" + + artifacts = http_get(data["artifacts_url"])["artifacts"] + comment_artifact = next(a for a in artifacts if a["name"] == COMMENT_FILE) + comment_url = comment_artifact["archive_download_url"] comment_zip = BytesIO(http_get(comment_url, is_json=False)) with zipfile.ZipFile(comment_zip) as zfile: with zfile.open(COMMENT_FILE) as rf: diff --git a/src/black/__init__.py b/src/black/__init__.py index 51e31e9598b..a82cf6a6cd7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -697,6 +697,9 @@ def reformat_code( report.failed(path, str(exc)) +# diff-shades depends on being to monkeypatch this function to operate. I know it's +# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 +@mypyc_attr(patchable=True) def reformat_one( src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report" ) -> None: From ac7402cbf6a0deb5c74e9abcffc5bd7b1148fda5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 13:21:13 -0400 Subject: [PATCH 201/700] Bump sphinx from 4.4.0 to 4.5.0 in /docs (GH-2959) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.4.0...v4.5.0) --- updated-dependencies: - dependency-name: sphinx 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 5ca7a6f1cf7..63c9c8f9edb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.16.1 -Sphinx==4.4.0 +Sphinx==4.5.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 furo==2022.3.4 From e9681a40dcb3d38b56b301d811bb1c55201fd97e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 28 Mar 2022 12:01:13 -0700 Subject: [PATCH 202/700] Fix _unicodefun patch code for Click 8.1.0 (#2966) Fixes #2964 --- .github/workflows/diff_shades.yml | 4 ++-- CHANGES.md | 1 + src/black/__init__.py | 14 +++++++++++--- tests/test_black.py | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 51fcebcff63..0529b137572 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -24,7 +24,7 @@ jobs: - name: Install diff-shades and support dependencies run: | - python -m pip install click packaging urllib3 + python -m pip install 'click<8.1.0' packaging urllib3 python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - name: Calculate run configuration & metadata @@ -59,7 +59,7 @@ jobs: - name: Install diff-shades and support dependencies run: | python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - python -m pip install click packaging urllib3 + python -m pip install 'click<8.1.0' packaging urllib3 python -m pip install -r .github/mypyc-requirements.txt # After checking out old revisions, this might not exist so we'll use a copy. cat scripts/diff_shades_gha_helper.py > helper.py diff --git a/CHANGES.md b/CHANGES.md index d34bd4ead58..02327fcbee9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,6 +55,7 @@ +- Fix Black to work with Click 8.1.0 (#2966) - On Python 3.11 and newer, use the standard library's `tomllib` instead of `tomli` (#2903) - `black-primer`, the deprecated internal devtool, has been removed and copied to a diff --git a/src/black/__init__.py b/src/black/__init__.py index a82cf6a6cd7..e9d3c1b795a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1427,13 +1427,21 @@ def patch_click() -> None: file paths is minimal since it's Python source code. Moreover, this crash was spurious on Python 3.7 thanks to PEP 538 and PEP 540. """ + modules: List[Any] = [] try: from click import core + except ImportError: + pass + else: + modules.append(core) + try: from click import _unicodefun - except ModuleNotFoundError: - return + except ImportError: + pass + else: + modules.append(_unicodefun) - for module in (core, _unicodefun): + for module in modules: if hasattr(module, "_verify_python3_env"): module._verify_python3_env = lambda: None # type: ignore if hasattr(module, "_verify_python_env"): diff --git a/tests/test_black.py b/tests/test_black.py index b1bf1772550..ce7bab2f440 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1257,7 +1257,7 @@ def test_assert_equivalent_different_asts(self) -> None: def test_shhh_click(self) -> None: try: from click import _unicodefun - except ModuleNotFoundError: + except ImportError: self.skipTest("Incompatible Click version") if not hasattr(_unicodefun, "_verify_python3_env"): self.skipTest("Incompatible Click version") From ae2c0758c9e61a385df9700dc9c231bf54887041 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 28 Mar 2022 12:08:29 -0700 Subject: [PATCH 203/700] Prepare release 22.3.0 (#2968) --- CHANGES.md | 57 +++++++++++++-------- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 02327fcbee9..f68bc8f1a99 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,11 +14,6 @@ -- Code cell separators `#%%` are now standardised to `# %%` (#2919) -- Remove unnecessary parentheses from `except` statements (#2939) -- Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) -- Avoid magic-trailing-comma in single-element subscripts (#2942) - ### _Blackd_ @@ -27,34 +22,62 @@ +### Documentation + + + +### Integrations + + + +### Output + + + +### Packaging + + + +### Parser + + + +### Performance + + + +## 22.3.0 + +### Preview style + +- Code cell separators `#%%` are now standardised to `# %%` (#2919) +- Remove unnecessary parentheses from `except` statements (#2939) +- Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) +- Avoid magic-trailing-comma in single-element subscripts (#2942) + +### Configuration + - 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) - Avoid crashing when md5 is not available (#2905) +- Fix handling of directory junctions on Windows (#2904) ### Documentation - - - Update pylint config documentation (#2931) ### Integrations - - - Move test to disable plugin in Vim/Neovim, which speeds up loading (#2896) ### Output - - - In verbose, mode, log when _Black_ is using user-level config (#2861) ### Packaging - - - Fix Black to work with Click 8.1.0 (#2966) - On Python 3.11 and newer, use the standard library's `tomllib` instead of `tomli` (#2903) @@ -66,12 +89,6 @@ - Black can now parse starred expressions in the target of `for` and `async for` statements, e.g `for item in *items_1, *items_2: pass` (#2879). -- Fix handling of directory junctions on Windows (#2904) - -### 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/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 7215e111f5c..d7d3da47630 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: 22.1.0 + rev: 22.3.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 48dda3ba036..4c793f459a2 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 22.1.0 +black, version 22.3.0 ``` An option to require a specific version to be running is also provided. From 2d62a09e838d2df98961e6e93036abad54f28c5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:42:53 -0400 Subject: [PATCH 204/700] Bump actions/cache from 2.1.7 to 3 (GH-2962) Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.7...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 0529b137572..611d8bc13dd 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -68,7 +68,7 @@ jobs: - name: Attempt to use cached baseline analysis id: baseline-cache - uses: actions/cache@v2.1.7 + uses: actions/cache@v3 with: path: ${{ matrix.baseline-analysis }} key: ${{ matrix.baseline-cache-key }} From 82e150a13ac78a1f24de27977d8a40883bc3d6e7 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 30 Mar 2022 16:40:50 -0400 Subject: [PATCH 205/700] Keep tests working w/ upcoming aiohttp 4.0.0 (#2974) aiohttp.test_utils.unittest_run_loop was deprecated since aiohttp 3.8 and aiohttp 4 (which isn't a thing quite yet) removes it. To maintain compatibility with the full range of versions we declare to support, test_blackd.py will now define a no-op replacement if it can't be imported. Also, mypy is painfully slow to use without a cache, let's reenable it. --- mypy.ini | 3 --- tests/test_blackd.py | 15 +++++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/mypy.ini b/mypy.ini index 3bb92a659ff..8ee9068d10b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -32,9 +32,6 @@ warn_unreachable=True disallow_untyped_defs=True check_untyped_defs=True -# No incremental mode -cache_dir=/dev/null - [mypy-black] # The following is because of `patch_click()`. Remove when # we drop Python 3.6 support. diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 37431fcad00..6174c4538b9 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,4 +1,5 @@ import re +from typing import Any from unittest.mock import patch from click.testing import CliRunner @@ -8,12 +9,18 @@ try: import blackd - from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + from aiohttp.test_utils import AioHTTPTestCase from aiohttp import web +except ImportError as e: + raise RuntimeError("Please install Black with the 'd' extra") from e + +try: + from aiohttp.test_utils import unittest_run_loop except ImportError: - has_blackd_deps = False -else: - has_blackd_deps = True + # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and aiohttp 4 + # removed it. To maintain compatibility we can make our own no-op decorator. + def unittest_run_loop(func: Any, *args: Any, **kwargs: Any) -> Any: + return func @pytest.mark.blackd From 3dea6e363562050ae032c80a648bd88fc49381bc Mon Sep 17 00:00:00 2001 From: "Gunung P. Wibisono" <55311527+gunungpw@users.noreply.github.com> Date: Thu, 31 Mar 2022 03:43:46 +0700 Subject: [PATCH 206/700] Convert `index.rst` and `license.rst` to markdown (#2852) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/conf.py | 4 + docs/contributing/index.md | 49 +++++++++ docs/contributing/index.rst | 43 -------- docs/guides/index.md | 16 +++ docs/guides/index.rst | 14 --- docs/index.md | 139 +++++++++++++++++++++++++ docs/index.rst | 131 ----------------------- docs/integrations/index.md | 31 ++++++ docs/integrations/index.rst | 28 ----- docs/license.md | 9 ++ docs/license.rst | 6 -- docs/the_black_code_style/index.md | 47 +++++++++ docs/the_black_code_style/index.rst | 47 --------- docs/usage_and_configuration/index.md | 28 +++++ docs/usage_and_configuration/index.rst | 26 ----- 15 files changed, 323 insertions(+), 295 deletions(-) create mode 100644 docs/contributing/index.md delete mode 100644 docs/contributing/index.rst create mode 100644 docs/guides/index.md delete mode 100644 docs/guides/index.rst create mode 100644 docs/index.md delete mode 100644 docs/index.rst create mode 100644 docs/integrations/index.md delete mode 100644 docs/integrations/index.rst create mode 100644 docs/license.md delete mode 100644 docs/license.rst create mode 100644 docs/the_black_code_style/index.md delete mode 100644 docs/the_black_code_style/index.rst create mode 100644 docs/usage_and_configuration/index.md delete mode 100644 docs/usage_and_configuration/index.rst diff --git a/docs/conf.py b/docs/conf.py index 2801e0eed19..e9fdebb5546 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -105,11 +105,15 @@ def make_pypi_svg(version: str) -> None: # Prettier support formatting some MyST syntax but not all, so let's disable the # unsupported yet still enabled by default ones. myst_disable_syntax = [ + "colon_fence", "myst_block_break", "myst_line_comment", "math_block", ] +# Optional MyST Syntaxes +myst_enable_extensions = [] + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/docs/contributing/index.md b/docs/contributing/index.md new file mode 100644 index 00000000000..f56e57c9e90 --- /dev/null +++ b/docs/contributing/index.md @@ -0,0 +1,49 @@ +# Contributing + +```{toctree} +--- +hidden: +--- + +the_basics +gauging_changes +issue_triage +release_process +reference/reference_summary +``` + +Welcome! Happy to see you willing to make the project better. Have you read the entire +[user documentation](https://black.readthedocs.io/en/latest/) yet? + +```{rubric} Bird's eye view + +``` + +In terms of inspiration, _Black_ is about as configurable as _gofmt_ (which is to say, +not very). This is deliberate. _Black_ aims to provide a consistent style and take away +opportunities for arguing about style. + +Bug reports and fixes are always welcome! Please follow the +[issue template on GitHub](https://github.com/psf/black/issues/new) for best results. + +Before you suggest a new feature or configuration knob, ask yourself why you want it. If +it enables better integration with some workflow, fixes an inconsistency, speeds things +up, and so on - go for it! On the other hand, if your answer is "because I don't like a +particular formatting" then you're not ready to embrace _Black_ yet. Such changes are +unlikely to get accepted. You can still try but prepare to be disappointed. + +```{rubric} Contents + +``` + +This section covers the following topics: + +- {doc}`the_basics` +- {doc}`gauging_changes` +- {doc}`release_process` +- {doc}`reference/reference_summary` + +For an overview on contributing to the _Black_, please checkout {doc}`the_basics`. + +If you need a reference of the functions, classes, etc. available to you while +developing _Black_, there's the {doc}`reference/reference_summary` docs. diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst deleted file mode 100644 index 480dbd6cd0e..00000000000 --- a/docs/contributing/index.rst +++ /dev/null @@ -1,43 +0,0 @@ -Contributing -============ - -.. toctree:: - :hidden: - - the_basics - gauging_changes - issue_triage - release_process - reference/reference_summary - -Welcome! Happy to see you willing to make the project better. Have you read the entire -`user documentation `_ yet? - -.. rubric:: Bird's eye view - -In terms of inspiration, *Black* is about as configurable as *gofmt* (which is to say, -not very). This is deliberate. *Black* aims to provide a consistent style and take away -opportunities for arguing about style. - -Bug reports and fixes are always welcome! Please follow the -`issue template on GitHub `_ for best results. - -Before you suggest a new feature or configuration knob, ask yourself why you want it. If -it enables better integration with some workflow, fixes an inconsistency, speeds things -up, and so on - go for it! On the other hand, if your answer is "because I don't like a -particular formatting" then you're not ready to embrace *Black* yet. Such changes are -unlikely to get accepted. You can still try but prepare to be disappointed. - -.. rubric:: Contents - -This section covers the following topics: - -- :doc:`the_basics` -- :doc:`gauging_changes` -- :doc:`release_process` -- :doc:`reference/reference_summary` - -For an overview on contributing to the *Black*, please checkout :doc:`the_basics`. - -If you need a reference of the functions, classes, etc. available to you while -developing *Black*, there's the :doc:`reference/reference_summary` docs. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 00000000000..127279b5e81 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,16 @@ +# Guides + +```{toctree} +--- +hidden: +--- + +introducing_black_to_your_project +using_black_with_other_tools +``` + +Wondering how to do something specific? You've found the right place! Listed below are +topic specific guides available: + +- {doc}`introducing_black_to_your_project` +- {doc}`using_black_with_other_tools` diff --git a/docs/guides/index.rst b/docs/guides/index.rst deleted file mode 100644 index 717c5c4d066..00000000000 --- a/docs/guides/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Guides -====== - -.. toctree:: - :hidden: - - introducing_black_to_your_project - using_black_with_other_tools - -Wondering how to do something specific? You've found the right place! Listed below -are topic specific guides available: - -- :doc:`introducing_black_to_your_project` -- :doc:`using_black_with_other_tools` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000000..9d0db465022 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,139 @@ + + +# 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 and mental energy for more important matters. + +_Black_ makes code review faster by producing the smallest diffs possible. Blackened +code looks the same regardless of the project you're reading. Formatting becomes +transparent after a while and you can focus on the content instead. + +Try it out now using the [Black Playground](https://black.vercel.app). + +```{admonition} Note - Black is now stable! +*Black* is [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, 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 +original (see the +[Pragmatism](./the_black_code_style/current_style.md#pragmatism) +section for details). If you're feeling confident, use `--fast`. +``` + +```{note} +{doc}`Black is licensed under the MIT license `. +``` + +## Testimonials + +**Mike Bayer**, author of [SQLAlchemy](https://www.sqlalchemy.org/): + +> _I can't think of any single tool in my entire programming career that has given me a +> bigger productivity increase by its introduction. I can now do refactorings in about +> 1% of the keystrokes that it would have taken me previously when we had no way for +> code to format itself._ + +**Dusty Phillips**, +[writer](https://smile.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=dusty+phillips): + +> _Black is opinionated so you don't have to be._ + +**Hynek Schlawack**, creator of [attrs](https://www.attrs.org/), core developer of +Twisted and CPython: + +> _An auto-formatter that doesn't suck is all I want for Xmas!_ + +**Carl Meyer**, [Django](https://www.djangoproject.com/) core developer: + +> _At least the name is good._ + +**Kenneth Reitz**, creator of [requests](http://python-requests.org/) and +[pipenv](https://docs.pipenv.org/): + +> _This vastly improves the formatting of our code. Thanks a ton!_ + +## Show your style + +Use the badge in your project's README.md: + +```md +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +``` + +Using the badge in README.rst: + +```rst +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +``` + +Looks like this: + +```{image} https://img.shields.io/badge/code%20style-black-000000.svg +:target: https://github.com/psf/black +``` + +## Contents + +```{toctree} +--- +maxdepth: 3 +includehidden: +--- + +the_black_code_style/index +``` + +```{toctree} +--- +maxdepth: 3 +includehidden: +caption: User Guide +--- + +getting_started +usage_and_configuration/index +integrations/index +guides/index +faq +``` + +```{toctree} +--- +maxdepth: 2 +includehidden: +caption: Development +--- + +contributing/index +change_log +authors +``` + +```{toctree} +--- +hidden: +caption: Project Links +--- + +GitHub +PyPI +Chat +``` + +# Indices and tables + +- {ref}`genindex` +- {ref}`search` diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 8a8da0d6127..00000000000 --- a/docs/index.rst +++ /dev/null @@ -1,131 +0,0 @@ -.. black documentation master file, created by - sphinx-quickstart on Fri Mar 23 10:53:30 2018. - -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 -and mental energy for more important matters. - -*Black* makes code review faster by producing the smallest diffs -possible. Blackened code looks the same regardless of the project -you're reading. Formatting becomes transparent after a while and you -can focus on the content instead. - -Try it out now using the `Black Playground `_. - -.. admonition:: Note - Black is now stable! - - *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. - 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 - original (see the - `Pragmatism <./the_black_code_style/current_style.html#pragmatism>`_ - section for details). If you're feeling confident, use ``--fast``. - -.. note:: - :doc:`Black is licensed under the MIT license `. - -Testimonials ------------- - -**Mike Bayer**, author of `SQLAlchemy `_: - - *I can't think of any single tool in my entire programming career that has given me a - bigger productivity increase by its introduction. I can now do refactorings in about - 1% of the keystrokes that it would have taken me previously when we had no way for - code to format itself.* - -**Dusty Phillips**, `writer `_: - - *Black is opinionated so you don't have to be.* - -**Hynek Schlawack**, creator of `attrs `_, core -developer of Twisted and CPython: - - *An auto-formatter that doesn't suck is all I want for Xmas!* - -**Carl Meyer**, `Django `_ core developer: - - *At least the name is good.* - -**Kenneth Reitz**, creator of `requests `_ -and `pipenv `_: - - *This vastly improves the formatting of our code. Thanks a ton!* - - -Show your style ---------------- - -Use the badge in your project's README.md: - -.. code-block:: md - - [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - - -Using the badge in README.rst: - -.. code-block:: rst - - .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - -Looks like this: - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - -Contents --------- - -.. toctree:: - :maxdepth: 3 - :includehidden: - - the_black_code_style/index - -.. toctree:: - :maxdepth: 3 - :includehidden: - :caption: User Guide - - getting_started - usage_and_configuration/index - integrations/index - guides/index - faq - -.. toctree:: - :maxdepth: 2 - :includehidden: - :caption: Development - - contributing/index - change_log - authors - -.. toctree:: - :hidden: - :caption: Project Links - - GitHub - PyPI - Chat - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/docs/integrations/index.md b/docs/integrations/index.md new file mode 100644 index 00000000000..33135d08f1a --- /dev/null +++ b/docs/integrations/index.md @@ -0,0 +1,31 @@ +# Integrations + +```{toctree} +--- +hidden: +--- + +editors +github_actions +source_version_control +``` + +_Black_ can be integrated into many environments, providing a better and smoother +experience. Documentation for integrating _Black_ with a tool can be found for the +following areas: + +- {doc}`Editor / IDE <./editors>` +- {doc}`GitHub Actions <./github_actions>` +- {doc}`Source version control <./source_version_control>` + +Editors and tools not listed will require external contributions. + +Patches welcome! ✨ 🍰 ✨ + +Any tool can pipe code through _Black_ using its stdio mode (just +[use `-` as the file name](https://www.tldp.org/LDP/abs/html/special-chars.html#DASHREF2)). +The formatted code will be returned on stdout (unless `--check` was passed). _Black_ +will still emit messages on stderr but that shouldn't affect your use case. + +This can be used for example with PyCharm's or IntelliJ's +[File Watchers](https://www.jetbrains.com/help/pycharm/file-watchers.html). diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst deleted file mode 100644 index ed62ebcf044..00000000000 --- a/docs/integrations/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -Integrations -============ - -.. toctree:: - :hidden: - - editors - github_actions - source_version_control - -*Black* can be integrated into many environments, providing a better and smoother experience. Documentation for integrating *Black* with a tool can be found for the -following areas: - -- :doc:`Editor / IDE <./editors>` -- :doc:`GitHub Actions <./github_actions>` -- :doc:`Source version control <./source_version_control>` - -Editors and tools not listed will require external contributions. - -Patches welcome! ✨ 🍰 ✨ - -Any tool can pipe code through *Black* using its stdio mode (just -`use \`-\` as the file name `_). -The formatted code will be returned on stdout (unless ``--check`` was passed). *Black* -will still emit messages on stderr but that shouldn't affect your use case. - -This can be used for example with PyCharm's or IntelliJ's -`File Watchers `_. diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 00000000000..132c95bfe2a --- /dev/null +++ b/docs/license.md @@ -0,0 +1,9 @@ +--- +orphan: true +--- + +# License + +```{include} ../LICENSE + +``` diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index 2dc20a24492..00000000000 --- a/docs/license.rst +++ /dev/null @@ -1,6 +0,0 @@ -:orphan: - -License -======= - -.. include:: ../LICENSE diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md new file mode 100644 index 00000000000..d1508552fee --- /dev/null +++ b/docs/the_black_code_style/index.md @@ -0,0 +1,47 @@ +# The Black Code Style + +```{toctree} +--- +hidden: +--- + +Current style +Future style +``` + +_Black_ is a PEP 8 compliant opinionated formatter with its own 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](https://github.com/psf/black/labels/T%3A%20design) issue label. + +## Stability Policy + +The following policy applies for the _Black_ code style, in non pre-release versions of +_Black_: + +- The same code, formatted with the same options, will produce the same output for all + releases in a given calendar year. + + This means projects can safely use `black ~= 22.0` without worrying about major + formatting changes disrupting their project in 2022. We may still fix bugs where + _Black_ crashes on some code, and make other improvements that do not affect + formatting. + +- The first release in a new calendar year _may_ contain formatting changes, although + these will be minimised as much as possible. This is to allow for improved formatting + enabled by newer Python language syntax as well as due to improvements in the + formatting logic. + +- The `--preview` flag is exempt from this policy. There are no guarantees around the + stability of the output with that flag passed into _Black_. This flag is intended for + allowing experimentation with the proposed changes to the _Black_ code style. + +Documentation for both the current and future styles can be found: + +- {doc}`current_style` +- {doc}`future_style` diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst deleted file mode 100644 index 511a6ecf099..00000000000 --- a/docs/the_black_code_style/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -The Black Code Style -==================== - -.. toctree:: - :hidden: - - Current style - Future style - -*Black* is a PEP 8 compliant opinionated formatter with its own 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 ----------------- - -The following policy applies for the *Black* code style, in non pre-release -versions of *Black*: - -- The same code, formatted with the same options, will produce the same - output for all releases in a given calendar year. - - This means projects can safely use `black ~= 22.0` without worrying about - major formatting changes disrupting their project in 2022. We may still - fix bugs where *Black* crashes on some code, and make other improvements - that do not affect formatting. - -- The first release in a new calendar year *may* contain formatting changes, - although these will be minimised as much as possible. This is to allow for - improved formatting enabled by newer Python language syntax as well as due - to improvements in the formatting logic. - -- The ``--preview`` flag is exempt from this policy. There are no guarantees - around the stability of the output with that flag passed into *Black*. This - flag is intended for allowing experimentation with the proposed changes to - the *Black* code style. - -Documentation for both the current and future styles can be found: - -- :doc:`current_style` -- :doc:`future_style` diff --git a/docs/usage_and_configuration/index.md b/docs/usage_and_configuration/index.md new file mode 100644 index 00000000000..1c86a49b686 --- /dev/null +++ b/docs/usage_and_configuration/index.md @@ -0,0 +1,28 @@ +# Usage and Configuration + +```{toctree} +--- +hidden: +--- + +the_basics +file_collection_and_discovery +black_as_a_server +black_docker_image +``` + +Sometimes, running _Black_ with its defaults and passing filepaths to it just won't cut +it. Passing each file using paths will become burdensome, and maybe you would like +_Black_ to not touch your files and just output diffs. And yes, you _can_ tweak certain +parts of _Black_'s style, but please know that configurability in this area is +purposefully limited. + +Using many of these more advanced features of _Black_ will require some configuration. +Configuration that will either live on the command line or in a TOML configuration file. + +This section covers features of _Black_ and configuring _Black_ in detail: + +- {doc}`The basics <./the_basics>` +- {doc}`File collection and discovery ` +- {doc}`Black as a server (blackd) <./black_as_a_server>` +- {doc}`Black Docker image <./black_docker_image>` diff --git a/docs/usage_and_configuration/index.rst b/docs/usage_and_configuration/index.rst deleted file mode 100644 index f6152eec90c..00000000000 --- a/docs/usage_and_configuration/index.rst +++ /dev/null @@ -1,26 +0,0 @@ -Usage and Configuration -======================= - -.. toctree:: - :hidden: - - the_basics - file_collection_and_discovery - black_as_a_server - black_docker_image - -Sometimes, running *Black* with its defaults and passing filepaths to it just won't cut -it. Passing each file using paths will become burdensome, and maybe you would like -*Black* to not touch your files and just output diffs. And yes, you *can* tweak certain -parts of *Black*'s style, but please know that configurability in this area is -purposefully limited. - -Using many of these more advanced features of *Black* will require some configuration. -Configuration that will either live on the command line or in a TOML configuration file. - -This section covers features of *Black* and configuring *Black* in detail: - -- :doc:`The basics <./the_basics>` -- :doc:`File collection and discovery ` -- :doc:`Black as a server (blackd) <./black_as_a_server>` -- :doc:`Black Docker image <./black_docker_image>` From a66016cb949590863d11e329e65bbb383727125e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 30 Mar 2022 14:01:03 -0700 Subject: [PATCH 207/700] Add # type: ignore for click._unicodefun import (#2981) --- src/black/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index e9d3c1b795a..bdeb73273bc 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1435,7 +1435,9 @@ def patch_click() -> None: else: modules.append(core) try: - from click import _unicodefun + # Removed in Click 8.1.0 and newer; we keep this around for users who have + # older versions installed. + from click import _unicodefun # type: ignore except ImportError: pass else: From def048356bb31474c3316f758503742773265bbf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 30 Mar 2022 14:33:16 -0700 Subject: [PATCH 208/700] Remove click pin in diff-shades workflow (#2979) Click 8.1.1 was released with a fix for pallets/click#2227. --- .github/workflows/diff_shades.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 611d8bc13dd..ade71e7aa8d 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -24,7 +24,7 @@ jobs: - name: Install diff-shades and support dependencies run: | - python -m pip install 'click<8.1.0' packaging urllib3 + python -m pip install click packaging urllib3 python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - name: Calculate run configuration & metadata @@ -59,7 +59,7 @@ jobs: - name: Install diff-shades and support dependencies run: | python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - python -m pip install 'click<8.1.0' packaging urllib3 + python -m pip install click packaging urllib3 python -m pip install -r .github/mypyc-requirements.txt # After checking out old revisions, this might not exist so we'll use a copy. cat scripts/diff_shades_gha_helper.py > helper.py From 3451502daa79746bb91fd3bd0697fb00d55d3a5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 11:22:52 -0400 Subject: [PATCH 209/700] Bump peter-evans/find-comment from 1.3.0 to 2 (#2960) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 1.3.0 to 2. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/d2dae40ed151c634e4189471272b57e76ec19ba8...1769778a0c5bd330272d749d12c036d65e70d39d) --- updated-dependencies: - dependency-name: peter-evans/find-comment dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index cf5d8bf9ac5..76067ce03a0 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -31,7 +31,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@d2dae40ed151c634e4189471272b57e76ec19ba8 + uses: peter-evans/find-comment@1769778a0c5bd330272d749d12c036d65e70d39d with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From 5436810921a2649a30f42b82ff6563a1af16d40f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 11:23:08 -0400 Subject: [PATCH 210/700] Bump peter-evans/create-or-update-comment from 1.4.5 to 2 (#2961) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 1.4.5 to 2. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/a35cf36e5301d70b76f316e867e7788a55a31dae...c9fcb64660bc90ec1cc535646af190c992007c32) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 76067ce03a0..94302735d0a 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -39,7 +39,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@a35cf36e5301d70b76f316e867e7788a55a31dae + uses: peter-evans/create-or-update-comment@c9fcb64660bc90ec1cc535646af190c992007c32 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From 1af29fbfa507daa8166e7aac659e9b2ff2b47a3c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 2 Apr 2022 08:29:32 -0700 Subject: [PATCH 211/700] try-except tomllib import (#2987) See #2965 I left the version check in place because mypy doesn't generally like try-excepted imports. --- CHANGES.md | 3 +++ setup.py | 2 +- src/black/files.py | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f68bc8f1a99..f2bdbd2e2b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,9 @@ +- Use `tomli` instead of `tomllib` on Python 3.11 builds where `tomllib` is not + available (#2987) + ### Parser diff --git a/setup.py b/setup.py index e23a58c411c..522a42a7ce2 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ def find_python_files(base: Path) -> List[Path]: install_requires=[ "click>=8.0.0", "platformdirs>=2", - "tomli>=1.1.0; python_version < '3.11'", + "tomli>=1.1.0; python_full_version < '3.11.0a7'", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "pathspec>=0.9.0", "dataclasses>=0.6; python_version < '3.7'", diff --git a/src/black/files.py b/src/black/files.py index 52c77c63346..0382397e8a2 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -22,7 +22,11 @@ from pathspec.patterns.gitwildmatch import GitWildMatchPatternError if sys.version_info >= (3, 11): - import tomllib + try: + import tomllib + except ImportError: + # Help users on older alphas + import tomli as tomllib else: import tomli as tomllib From 4d0a4b15a935c5363563a0e0b570c4467943abb9 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Sun, 3 Apr 2022 00:18:13 +0100 Subject: [PATCH 212/700] Fix broken link in README.md (#2989) Broken when we converted some more RST docs to MyST --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2d06bedad2..5b71ba72143 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ tests, and our own auto formatting and parallel Continuous Integration runner. N 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). +[The Black Code Style](https://black.readthedocs.io/en/stable/the_black_code_style/index.html). 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 24c708eb374a856372284fb1a4f021fec292f713 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Sun, 3 Apr 2022 04:27:33 +0100 Subject: [PATCH 213/700] Remove unnecessary parentheses from `with` statements (#2926) Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 2 + src/black/linegen.py | 98 +++++++++++++++++++----- tests/data/remove_for_brackets.py | 8 ++ tests/data/remove_with_brackets.py | 119 +++++++++++++++++++++++++++++ tests/test_format.py | 10 +++ 5 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 tests/data/remove_with_brackets.py diff --git a/CHANGES.md b/CHANGES.md index f2bdbd2e2b7..30c00566b3c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ +- Remove unnecessary parentheses from `with` statements (#2926) + ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 8a28c3901bb..2cf9cf3130a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -322,9 +322,10 @@ def __post_init__(self) -> None: self.visit_except_clause = partial( v, keywords={"except"}, parens={"except"} ) + self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) else: self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) - self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) + self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) @@ -845,11 +846,26 @@ def normalize_invisible_parens( check_lpar = True if check_lpar: - if child.type == syms.atom: + if ( + preview + and child.type == syms.atom + and node.type == syms.for_stmt + and isinstance(child.prev_sibling, Leaf) + and child.prev_sibling.type == token.NAME + and child.prev_sibling.value == "for" + ): + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(node, child, visible=False) + elif preview and isinstance(child, Node) and node.type == syms.with_stmt: + remove_with_parens(child, node) + elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom( child, parent=node, - preview=preview, ): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): @@ -871,38 +887,78 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - check_lpar = isinstance(child, Leaf) and child.value in parens_after + comma_check = child.type == token.COMMA if preview else False + + check_lpar = isinstance(child, Leaf) and ( + child.value in parens_after or comma_check + ) + + +def remove_with_parens(node: Node, parent: Node) -> None: + """Recursively hide optional parens in `with` statements.""" + # Removing all unnecessary parentheses in with statements in one pass is a tad + # complex as different variations of bracketed statements result in pretty + # different parse trees: + # + # with (open("file")) as f: # this is an asexpr_test + # ... + # + # with (open("file") as f): # this is an atom containing an + # ... # asexpr_test + # + # with (open("file")) as f, (open("file")) as f: # this is asexpr_test, COMMA, + # ... # asexpr_test + # + # with (open("file") as f, open("file") as f): # an atom containing a + # ... # testlist_gexp which then + # # contains multiple asexpr_test(s) + if node.type == syms.atom: + if maybe_make_parens_invisible_in_atom( + node, + parent=parent, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(parent, node, visible=False) + if isinstance(node.children[1], Node): + remove_with_parens(node.children[1], node) + elif node.type == syms.testlist_gexp: + for child in node.children: + if isinstance(child, Node): + remove_with_parens(child, node) + elif node.type == syms.asexpr_test and not any( + leaf.type == token.COLONEQUAL for leaf in node.leaves() + ): + if maybe_make_parens_invisible_in_atom( + node.children[0], + parent=node, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(node, node.children[0], visible=False) def maybe_make_parens_invisible_in_atom( node: LN, parent: LN, - preview: bool = False, + remove_brackets_around_comma: bool = False, ) -> bool: """If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` as they are redundant. Returns whether the node should itself be wrapped in invisible parentheses. - """ - if ( - preview - and parent.type == syms.for_stmt - and isinstance(node.prev_sibling, Leaf) - and node.prev_sibling.type == token.NAME - and node.prev_sibling.value == "for" - ): - for_stmt_check = False - else: - for_stmt_check = True - if ( node.type != syms.atom or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and for_stmt_check) + or ( + # This condition tries to prevent removing non-optional brackets + # around a tuple, however, can be a bit overzealous so we provide + # and option to skip this check for `for` and `with` statements. + not remove_brackets_around_comma + and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + ) ): return False @@ -925,7 +981,11 @@ def maybe_make_parens_invisible_in_atom( # make parentheses invisible first.value = "" last.value = "" - maybe_make_parens_invisible_in_atom(middle, parent=parent, preview=preview) + maybe_make_parens_invisible_in_atom( + middle, + parent=parent, + remove_brackets_around_comma=remove_brackets_around_comma, + ) if is_atom_with_invisible_parens(middle): # Strip the invisible parens from `middle` by replacing diff --git a/tests/data/remove_for_brackets.py b/tests/data/remove_for_brackets.py index c8d88abcc50..cd5340462da 100644 --- a/tests/data/remove_for_brackets.py +++ b/tests/data/remove_for_brackets.py @@ -14,6 +14,10 @@ for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): print(k, v) +# Test deeply nested brackets +for (((((k, v))))) in d.items(): + print(k, v) + # output # Only remove tuple brackets after `for` for k, v in d.items(): @@ -38,3 +42,7 @@ dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() ): print(k, v) + +# Test deeply nested brackets +for k, v in d.items(): + print(k, v) diff --git a/tests/data/remove_with_brackets.py b/tests/data/remove_with_brackets.py new file mode 100644 index 00000000000..ea58ab93a16 --- /dev/null +++ b/tests/data/remove_with_brackets.py @@ -0,0 +1,119 @@ +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... + +# output +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... diff --git a/tests/test_format.py b/tests/test_format.py index a995bd3f1f5..d80eaa730cd 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -192,6 +192,16 @@ def test_pep_570() -> None: assert_format(source, expected, minimum_version=(3, 8)) +def test_remove_with_brackets() -> None: + source, expected = read_data("remove_with_brackets") + assert_format( + source, + expected, + black.Mode(preview=True), + minimum_version=(3, 9), + ) + + @pytest.mark.parametrize("filename", PY310_CASES) def test_python_310(filename: str) -> None: source, expected = read_data(filename) From fa5fd262fffd577e3c5d573af9c2fa0af2991be1 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 4 Apr 2022 21:23:30 -0400 Subject: [PATCH 214/700] Update test_black.shhh_click test for click 8+ (#2993) The 8.0.x series renamed its "die on LANG=C" function and the 8.1.x series straight up deleted it. Unfortunately this makes this test type check cleanly hard, so we'll just lint with click 8.1+ (the pre-commit hook configuration was changed mostly to just evict any now unsupported mypy environments) --- .pre-commit-config.yaml | 2 +- tests/test_black.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b96bc62fe17..26b7fe8c791 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: - types-PyYAML - tomli >= 0.2.6, < 2.0.0 - types-typed-ast >= 1.4.1 - - click >= 8.0.0 + - click >= 8.1.0 - platformdirs >= 2.1.0 - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/tests/test_black.py b/tests/test_black.py index ce7bab2f440..20cc9f7379f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1256,23 +1256,25 @@ def test_assert_equivalent_different_asts(self) -> None: def test_shhh_click(self) -> None: try: - from click import _unicodefun + from click import _unicodefun # type: ignore except ImportError: self.skipTest("Incompatible Click version") - if not hasattr(_unicodefun, "_verify_python3_env"): + + if not hasattr(_unicodefun, "_verify_python_env"): self.skipTest("Incompatible Click version") + # First, let's see if Click is crashing with a preferred ASCII charset. with patch("locale.getpreferredencoding") as gpe: gpe.return_value = "ASCII" with self.assertRaises(RuntimeError): - _unicodefun._verify_python3_env() # type: ignore + _unicodefun._verify_python_env() # Now, let's silence Click... black.patch_click() # ...and confirm it's silent. with patch("locale.getpreferredencoding") as gpe: gpe.return_value = "ASCII" try: - _unicodefun._verify_python3_env() # type: ignore + _unicodefun._verify_python_env() except RuntimeError as re: self.fail(f"`patch_click()` failed, exception still raised: {re}") From 421383d5607da1191eaf3682750278750ea33d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Tue, 5 Apr 2022 03:24:16 +0200 Subject: [PATCH 215/700] Update FAQ: Mention formatting of custom jupyter cell magic (#2982) Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/faq.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 70f9b51394f..a5919a39af5 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -57,7 +57,8 @@ _Black_ is timid about formatting Jupyter Notebooks. Cells containing any of the following will not be formatted: - automagics (e.g. `pip install black`) -- non-Python cell magics (e.g. `%%writeline`) +- non-Python cell magics (e.g. `%%writeline`). These can be added with the flag + `--python-cell-magics`, e.g. `black --python-cell-magics writeline hello.ipynb`. - multiline magics, e.g.: ```python From 9b307405fb6d4248e1a1dd7c6c10fa02b3c347f0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 6 Apr 2022 15:48:50 +0300 Subject: [PATCH 216/700] Top PyPI Packages: Use 30-days data, 365 is no longer available (#2995) --- gallery/gallery.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/gallery/gallery.py b/gallery/gallery.py index 3df05c1a722..be4d81dc427 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -10,10 +10,9 @@ from concurrent.futures import ThreadPoolExecutor from functools import lru_cache, partial from pathlib import Path -from typing import ( # type: ignore # typing can't see Literal +from typing import ( Generator, List, - Literal, NamedTuple, Optional, Tuple, @@ -24,12 +23,11 @@ PYPI_INSTANCE = "https://pypi.org/pypi" PYPI_TOP_PACKAGES = ( - "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-{days}-days.json" + "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json" ) INTERNAL_BLACK_REPO = f"{tempfile.gettempdir()}/__black" ArchiveKind = Union[tarfile.TarFile, zipfile.ZipFile] -Days = Union[Literal[30], Literal[365]] subprocess.run = partial(subprocess.run, check=True) # type: ignore # https://github.com/python/mypy/issues/1484 @@ -64,8 +62,8 @@ def get_pypi_download_url(package: str, version: Optional[str]) -> str: return cast(str, source["url"]) -def get_top_packages(days: Days) -> List[str]: - with urlopen(PYPI_TOP_PACKAGES.format(days=days)) as page: +def get_top_packages() -> List[str]: + with urlopen(PYPI_TOP_PACKAGES) as page: result = json.load(page) return [package["project"] for package in result["rows"]] @@ -128,13 +126,12 @@ def get_package( def download_and_extract_top_packages( directory: Path, - days: Days = 365, workers: int = 8, limit: slice = DEFAULT_SLICE, ) -> Generator[Path, None, None]: with ThreadPoolExecutor(max_workers=workers) as executor: bound_downloader = partial(get_package, version=None, directory=directory) - for package in executor.map(bound_downloader, get_top_packages(days)[limit]): + for package in executor.map(bound_downloader, get_top_packages()[limit]): if package is not None: yield package From f6188ce6dcde3fcad381c52ecc74374e7d0579c9 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Wed, 6 Apr 2022 19:04:12 +0100 Subject: [PATCH 217/700] Output python version and implementation as part of `--version` flag (#2997) Example: black, 22.1.1.dev56+g421383d.d20220405 (compiled: no) Python (CPython) 3.9.12 Co-authored-by: Batuhan Taskaya --- CHANGES.md | 2 ++ src/black/__init__.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 30c00566b3c..3bf481fb580 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,8 @@ +- Output python version and implementation as part of `--version` flag (#2997) + ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index bdeb73273bc..3a2d1cb8898 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -10,6 +10,7 @@ import os from pathlib import Path from pathspec.patterns.gitwildmatch import GitWildMatchPatternError +import platform import re import signal import sys @@ -381,7 +382,10 @@ def validate_regex( ) @click.version_option( version=__version__, - message=f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})", + message=( + f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n" + f"Python ({platform.python_implementation()}) {platform.python_version()}" + ), ) @click.argument( "src", From 98fcccee55acba04afdd933676f56927cfe9bbe4 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Sat, 9 Apr 2022 15:36:05 +0100 Subject: [PATCH 218/700] Better manage return annotation brackets (#2990) Allows us to better control placement of return annotations by: a) removing redundant parens b) moves very long type annotations onto their own line Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/linegen.py | 31 +++- src/black/mode.py | 1 + tests/data/return_annotation_brackets.py | 222 +++++++++++++++++++++++ tests/test_format.py | 1 + 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 tests/data/return_annotation_brackets.py diff --git a/CHANGES.md b/CHANGES.md index 3bf481fb580..c631aec7a3b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ +- Parentheses around return annotations are now managed (#2990) - Remove unnecessary parentheses from `with` statements (#2926) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 2cf9cf3130a..c2b0616d02f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -144,6 +144,33 @@ def visit_stmt( yield from self.visit(child) + def visit_funcdef(self, node: Node) -> Iterator[Line]: + """Visit function definition.""" + if Preview.annotation_parens not in self.mode: + yield from self.visit_stmt(node, keywords={"def"}, parens=set()) + else: + yield from self.line() + + # Remove redundant brackets around return type annotation. + is_return_annotation = False + for child in node.children: + if child.type == token.RARROW: + is_return_annotation = True + elif is_return_annotation: + if child.type == syms.atom and child.children[0].type == token.LPAR: + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_brackets_around_comma=False, + ): + wrap_in_parentheses(node, child, visible=False) + else: + wrap_in_parentheses(node, child, visible=False) + is_return_annotation = False + + for child in node.children: + yield from self.visit(child) + def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) @@ -326,7 +353,6 @@ def __post_init__(self) -> None: else: self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) - self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) @@ -478,7 +504,10 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator current_leaves is body_leaves and leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is matching_bracket + and isinstance(matching_bracket, Leaf) ): + ensure_visible(leaf) + ensure_visible(matching_bracket) current_leaves = tail_leaves if body_leaves else head_leaves current_leaves.append(leaf) if current_leaves is head_leaves: diff --git a/src/black/mode.py b/src/black/mode.py index 6b74c14b6de..34905702a54 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -129,6 +129,7 @@ class Preview(Enum): string_processing = auto() remove_redundant_parens = auto() one_element_subscript = auto() + annotation_parens = auto() class Deprecated(UserWarning): diff --git a/tests/data/return_annotation_brackets.py b/tests/data/return_annotation_brackets.py new file mode 100644 index 00000000000..27760bd51d7 --- /dev/null +++ b/tests/data/return_annotation_brackets.py @@ -0,0 +1,222 @@ +# Control +def double(a: int) -> int: + return 2*a + +# Remove the brackets +def double(a: int) -> (int): + return 2*a + +# Some newline variations +def double(a: int) -> ( + int): + return 2*a + +def double(a: int) -> (int +): + return 2*a + +def double(a: int) -> ( + int +): + return 2*a + +# Don't lose the comments +def double(a: int) -> ( # Hello + int +): + return 2*a + +def double(a: int) -> ( + int # Hello +): + return 2*a + +# Really long annotations +def foo() -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: + return 2 + +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: + return 2 + +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: + return 2 + +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: + return 2 + +# Split args but no need to split return +def foo(a: int, b: int, c: int,) -> int: + return 2 + +# Deeply nested brackets +# with *interesting* spacing +def double(a: int) -> (((((int))))): + return 2*a + +def double(a: int) -> ( + ( ( + ((int) + ) + ) + ) + ): + return 2*a + +def foo() -> ( + ( ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +) +)): + return 2 + +# Return type with commas +def foo() -> ( + tuple[int, int, int] +): + return 2 + +def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]: + return 2 + +# Magic trailing comma example +def foo() -> tuple[int, int, int,]: + return 2 + +# Long string example +def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": + pass + +# output +# Control +def double(a: int) -> int: + return 2 * a + + +# Remove the brackets +def double(a: int) -> int: + return 2 * a + + +# Some newline variations +def double(a: int) -> int: + return 2 * a + + +def double(a: int) -> int: + return 2 * a + + +def double(a: int) -> int: + return 2 * a + + +# Don't lose the comments +def double(a: int) -> int: # Hello + return 2 * a + + +def double(a: int) -> int: # Hello + return 2 * a + + +# Really long annotations +def foo() -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + + +def foo() -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + + +def foo() -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds + | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + + +def foo( + a: int, + b: int, + c: int, +) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: + return 2 + + +def foo( + a: int, + b: int, + c: int, +) -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds + | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + + +# Split args but no need to split return +def foo( + a: int, + b: int, + c: int, +) -> int: + return 2 + + +# Deeply nested brackets +# with *interesting* spacing +def double(a: int) -> int: + return 2 * a + + +def double(a: int) -> int: + return 2 * a + + +def foo() -> ( + intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds +): + return 2 + + +# Return type with commas +def foo() -> tuple[int, int, int]: + return 2 + + +def foo() -> ( + tuple[ + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + ] +): + return 2 + + +# Magic trailing comma example +def foo() -> ( + tuple[ + int, + int, + int, + ] +): + return 2 + + +# Long string example +def frobnicate() -> ( + "ThisIsTrulyUnreasonablyExtremelyLongClassName |" + " list[ThisIsTrulyUnreasonablyExtremelyLongClassName]" +): + pass diff --git a/tests/test_format.py b/tests/test_format.py index d80eaa730cd..6f71617eee6 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -83,6 +83,7 @@ "remove_except_parens", "remove_for_brackets", "one_element_subscript", + "return_annotation_brackets", ] SOURCES: List[str] = [ From 75f99bded33abe962ca08bf16c77635ac9ca00a1 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:49:40 +0100 Subject: [PATCH 219/700] Remove redundant parentheses around awaited coroutines/tasks (#2991) This is a tricky one as await is technically an expression and therefore in certain situations requires brackets for operator precedence. However, the vast majority of await usage is just await some_coroutine(...) and similar in format to return statements. Therefore this PR removes redundant parens around these await expressions. Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + src/black/linegen.py | 41 +++++++- tests/data/remove_await_parens.py | 168 ++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 tests/data/remove_await_parens.py diff --git a/CHANGES.md b/CHANGES.md index c631aec7a3b..e168e24d76e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ +- Remove redundant parentheses around awaited objects (#2991) - Parentheses around return annotations are now managed (#2990) - Remove unnecessary parentheses from `with` statements (#2926) diff --git a/src/black/linegen.py b/src/black/linegen.py index c2b0616d02f..caffbab0cbc 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -3,7 +3,7 @@ """ from functools import partial, wraps import sys -from typing import Collection, Iterator, List, Optional, Set, Union +from typing import Collection, Iterator, List, Optional, Set, Union, cast from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS @@ -253,6 +253,9 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) + if Preview.remove_redundant_parens in self.mode: + remove_await_parens(node) + yield from self.visit_default(node) def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: @@ -923,6 +926,42 @@ def normalize_invisible_parens( ) +def remove_await_parens(node: Node) -> None: + if node.children[0].type == token.AWAIT and len(node.children) > 1: + if ( + node.children[1].type == syms.atom + and node.children[1].children[0].type == token.LPAR + ): + if maybe_make_parens_invisible_in_atom( + node.children[1], + parent=node, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(node, node.children[1], visible=False) + + # Since await is an expression we shouldn't remove + # brackets in cases where this would change + # the AST due to operator precedence. + # Therefore we only aim to remove brackets around + # power nodes that aren't also await expressions themselves. + # https://peps.python.org/pep-0492/#updated-operator-precedence-table + # N.B. We've still removed any redundant nested brackets though :) + opening_bracket = cast(Leaf, node.children[1].children[0]) + closing_bracket = cast(Leaf, node.children[1].children[-1]) + bracket_contents = cast(Node, node.children[1].children[1]) + if bracket_contents.type != syms.power: + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + elif ( + bracket_contents.type == syms.power + and bracket_contents.children[0].type == token.AWAIT + ): + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + # If we are in a nested await then recurse down. + remove_await_parens(bracket_contents) + + def remove_with_parens(node: Node, parent: Node) -> None: """Recursively hide optional parens in `with` statements.""" # Removing all unnecessary parentheses in with statements in one pass is a tad diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py new file mode 100644 index 00000000000..eb7dad340c3 --- /dev/null +++ b/tests/data/remove_await_parens.py @@ -0,0 +1,168 @@ +import asyncio + +# Control example +async def main(): + await asyncio.sleep(1) + +# Remove brackets for short coroutine/task +async def main(): + await (asyncio.sleep(1)) + +async def main(): + await ( + asyncio.sleep(1) + ) + +async def main(): + await (asyncio.sleep(1) + ) + +# Check comments +async def main(): + await ( # Hello + asyncio.sleep(1) + ) + +async def main(): + await ( + asyncio.sleep(1) # Hello + ) + +async def main(): + await ( + asyncio.sleep(1) + ) # Hello + +# Long lines +async def main(): + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + +# Same as above but with magic trailing comma in function +async def main(): + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + +# Cr@zY Br@ck3Tz +async def main(): + await ( + ((((((((((((( + ((( ((( + ((( ((( + ((( ((( + ((( ((( + ((black(1))) + ))) ))) + ))) ))) + ))) ))) + ))) ))) + ))))))))))))) + ) + +# Keep brackets around non power operations and nested awaits +async def main(): + await (set_of_tasks | other_set) + +async def main(): + await (await asyncio.sleep(1)) + +# It's awaits all the way down... +async def main(): + await (await x) + +async def main(): + await (yield x) + +async def main(): + await (await (asyncio.sleep(1))) + +async def main(): + await (await (await (await (await (asyncio.sleep(1)))))) + +# output +import asyncio + +# Control example +async def main(): + await asyncio.sleep(1) + + +# Remove brackets for short coroutine/task +async def main(): + await asyncio.sleep(1) + + +async def main(): + await asyncio.sleep(1) + + +async def main(): + await asyncio.sleep(1) + + +# Check comments +async def main(): + await asyncio.sleep(1) # Hello + + +async def main(): + await asyncio.sleep(1) # Hello + + +async def main(): + await asyncio.sleep(1) # Hello + + +# Long lines +async def main(): + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + + +# Same as above but with magic trailing comma in function +async def main(): + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + + +# Cr@zY Br@ck3Tz +async def main(): + await black(1) + + +# Keep brackets around non power operations and nested awaits +async def main(): + await (set_of_tasks | other_set) + + +async def main(): + await (await asyncio.sleep(1)) + + +# It's awaits all the way down... +async def main(): + await (await x) + + +async def main(): + await (yield x) + + +async def main(): + await (await asyncio.sleep(1)) + + +async def main(): + await (await (await (await (await asyncio.sleep(1))))) diff --git a/tests/test_format.py b/tests/test_format.py index 6f71617eee6..51d8fb0a103 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -83,6 +83,7 @@ "remove_except_parens", "remove_for_brackets", "one_element_subscript", + "remove_await_parens", "return_annotation_brackets", ] From 431bd09e15247431056894bd6444dee7c22893f0 Mon Sep 17 00:00:00 2001 From: Ryan Siu Date: Sat, 9 Apr 2022 16:52:45 -0400 Subject: [PATCH 220/700] Correctly handle fmt: skip comments without internal spaces (#2970) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 +++ src/black/comments.py | 7 +++++-- tests/data/fmtskip7.py | 11 +++++++++++ tests/test_format.py | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/data/fmtskip7.py diff --git a/CHANGES.md b/CHANGES.md index e168e24d76e..b21c319d5e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ +- Fix unstable formatting involving `# fmt: skip` comments without internal spaces + (#2970) + ### Preview style diff --git a/src/black/comments.py b/src/black/comments.py index 455326469f0..23bf87fca7c 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -214,8 +214,11 @@ def generate_ignored_nodes( container: Optional[LN] = container_of(leaf) if comment.value in FMT_SKIP: prev_sibling = leaf.prev_sibling - if comment.value in leaf.prefix and prev_sibling is not None: - leaf.prefix = leaf.prefix.replace(comment.value, "") + # Need to properly format the leaf prefix to compare it to comment.value, + # which is also formatted + comments = list_comments(leaf.prefix, is_endmarker=False, preview=preview) + if comments and comment.value == comments[0].value and prev_sibling is not None: + leaf.prefix = "" siblings = [prev_sibling] while ( "\n" not in prev_sibling.prefix diff --git a/tests/data/fmtskip7.py b/tests/data/fmtskip7.py new file mode 100644 index 00000000000..15ac0ad7080 --- /dev/null +++ b/tests/data/fmtskip7.py @@ -0,0 +1,11 @@ +a = "this is some code" +b = 5 #fmt:skip +c = 9 #fmt: skip +d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip + +# output + +a = "this is some code" +b = 5 # fmt:skip +c = 9 # fmt: skip +d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip diff --git a/tests/test_format.py b/tests/test_format.py index 51d8fb0a103..fd5f596b6d5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -44,6 +44,7 @@ "fmtskip4", "fmtskip5", "fmtskip6", + "fmtskip7", "fstring", "function", "function2", From 497a72560dd3612a062cbb0d8cb2f8c3c93b74ff Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 10 Apr 2022 19:45:34 -0400 Subject: [PATCH 221/700] Explain our use of mypyc in the FAQ (#3002) I realized we don't have a FAQ entry about this, let's change that so compiled: yes/no doesn't surprise as many people :) Co-authored-by: Jelle Zijlstra --- docs/faq.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index a5919a39af5..b2fe42de282 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -113,3 +113,22 @@ _Black_ is an autoformatter, not a Python linter or interpreter. Detecting all s errors is not a goal. It can format all code accepted by CPython (if you find an example where that doesn't hold, please report a bug!), but it may also format some code that CPython doesn't accept. + +## What is `compiled: yes/no` all about in the version output? + +While _Black_ is indeed a pure Python project, we use [mypyc] to compile _Black_ into a +C Python extension, usually doubling performance. These compiled wheels are available +for 64-bit versions of Windows, Linux (via the manylinux standard), and macOS across all +supported CPython versions. + +Platforms including musl-based and/or ARM Linux distributions, and ARM Windows are +currently **not** supported. These platforms will fall back to the slower pure Python +wheel available on PyPI. + +If you are experiencing exceptionally weird issues or even segfaults, you can try +passing `--no-binary black` to your pip install invocation. This flag excludes all +wheels (including the pure Python wheel), so this command will use the [sdist]. + +[mypyc]: https://mypyc.readthedocs.io/en/latest/ +[sdist]: + https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist From abdc31cd4fb6cf88339f8ade0adf9c5356a95aa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 17:26:28 -0400 Subject: [PATCH 222/700] Bump actions/upload-artifact from 2 to 3 (#3004) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index ade71e7aa8d..749f87cdcdb 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -108,19 +108,19 @@ jobs: ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} - name: Upload diff report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.mode }}-diff.html path: diff.html - name: Upload baseline analysis - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.baseline-analysis }} path: ${{ matrix.baseline-analysis }} - name: Upload target analysis - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.target-analysis }} path: ${{ matrix.target-analysis }} @@ -135,7 +135,7 @@ jobs: - name: Upload summary file (PR only) if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: .pr-comment.json path: .pr-comment.json From 40053b522ebe8f33328c1c7e29780fc37d17911e Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 11 Apr 2022 23:10:46 +0100 Subject: [PATCH 223/700] Quote "black[jupyter]" in README.md (#3007) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b71ba72143..1ffa7ef9522 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation _Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`. +run. If you want to format Jupyter Notebooks, install with +`pip install 'black[jupyter]'`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: From 911b59fb4f39a612f0c9e5350b78d7b37f5ed491 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 19:24:00 -0400 Subject: [PATCH 224/700] Bump furo from 2022.3.4 to 2022.4.7 in /docs (#3003) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.3.4 to 2022.4.7. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.03.04...2022.04.07) --- updated-dependencies: - dependency-name: furo 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 63c9c8f9edb..818415c6cf8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ myst-parser==0.16.1 Sphinx==4.5.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.3.4 +furo==2022.4.7 From 96bd428524763fc443ac1729e7254ccbe872d5ca Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Apr 2022 16:25:46 -0700 Subject: [PATCH 225/700] Quote black[jupyter] and black[d] in installation docs (#3006) We just got someone on Discord who was confused because the command as written caused their shell to try to do command expansion. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/getting_started.md | 3 ++- docs/usage_and_configuration/black_as_a_server.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 1227f653757..fca960915a8 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -17,7 +17,8 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation _Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`. +run. If you want to format Jupyter Notebooks, install with +`pip install 'black[jupyter]'`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index 75a4d925a54..7d07e94e6bb 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -7,7 +7,7 @@ process every time you want to blacken a file. ## Usage `blackd` is not packaged alongside _Black_ by default because it has additional -dependencies. You will need to execute `pip install black[d]` to install it. +dependencies. You will need to execute `pip install 'black[d]'` to install it. You can start the server on the default port, binding only to the local interface by running `blackd`. You will see a single line mentioning the server's version, and the From 712f8b37fb12a40ec6ea86903f44c2d0750f56a3 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Thu, 14 Apr 2022 00:13:33 +0100 Subject: [PATCH 226/700] Make ipynb tests compatible with ipython 8.3.0+ (#3008) --- tests/test_ipynb.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index b534d77c22a..6f6b3090cd1 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -1,3 +1,4 @@ +import contextlib from dataclasses import replace import pathlib import re @@ -18,6 +19,8 @@ from _pytest.monkeypatch import MonkeyPatch from tests.util import DATA_DIR +with contextlib.suppress(ModuleNotFoundError): + import IPython pytestmark = pytest.mark.jupyter pytest.importorskip("IPython", reason="IPython is an optional dependency") pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency") @@ -139,10 +142,15 @@ def test_non_python_magics(src: str) -> None: format_cell(src, fast=True, mode=JUPYTER_MODE) +@pytest.mark.skipif( + IPython.version_info < (8, 3), + reason="Change in how TransformerManager transforms this input", +) def test_set_input() -> None: src = "a = b??" - with pytest.raises(NothingChanged): - format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = "??b" + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + assert result == expected def test_input_already_contains_transformed_magic() -> None: From 7f7673d941a947a8d392c8c0866d3d588affc174 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 15 Apr 2022 19:25:07 +0300 Subject: [PATCH 227/700] Support 3.11 / PEP 654 syntax (#3016) --- CHANGES.md | 3 + src/black/__init__.py | 7 +++ src/black/linegen.py | 9 +++ src/black/mode.py | 17 ++++++ src/black/nodes.py | 4 ++ src/blib2to3/Grammar.txt | 2 +- tests/data/pep_654.py | 53 +++++++++++++++++ tests/data/pep_654_style.py | 111 ++++++++++++++++++++++++++++++++++++ tests/test_black.py | 6 ++ tests/test_format.py | 12 ++++ 10 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 tests/data/pep_654.py create mode 100644 tests/data/pep_654_style.py diff --git a/CHANGES.md b/CHANGES.md index b21c319d5e0..566077b1dbc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,9 @@ ### Parser +- [PEP 654](https://peps.python.org/pep-0654/#except) syntax (for example, + `except *ExceptionGroup:`) is now supported (#3016) + ### Performance diff --git a/src/black/__init__.py b/src/black/__init__.py index 3a2d1cb8898..3a1ce24f059 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1296,6 +1296,13 @@ def get_features_used( # noqa: C901 ): features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) + elif ( + n.type == syms.except_clause + and len(n.children) >= 2 + and n.children[1].type == token.STAR + ): + features.add(Feature.EXCEPT_STAR) + return features diff --git a/src/black/linegen.py b/src/black/linegen.py index caffbab0cbc..91fdeef8f2f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -915,6 +915,15 @@ def normalize_invisible_parens( node.insert_child(index, Leaf(token.LPAR, "")) node.append_child(Leaf(token.RPAR, "")) break + elif ( + index == 1 + and child.type == token.STAR + and node.type == syms.except_clause + ): + # In except* (PEP 654), the star is actually part of + # of the keyword. So we need to skip the insertion of + # invisible parentheses to work more precisely. + continue elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) diff --git a/src/black/mode.py b/src/black/mode.py index 34905702a54..6bd4ce14421 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -30,6 +30,7 @@ class TargetVersion(Enum): PY38 = 8 PY39 = 9 PY310 = 10 + PY311 = 11 class Feature(Enum): @@ -47,6 +48,7 @@ class Feature(Enum): PATTERN_MATCHING = 11 UNPACKING_ON_FLOW = 12 ANN_ASSIGN_EXTENDED_RHS = 13 + EXCEPT_STAR = 14 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -116,6 +118,21 @@ class Feature(Enum): Feature.ANN_ASSIGN_EXTENDED_RHS, Feature.PATTERN_MATCHING, }, + TargetVersion.PY311: { + Feature.F_STRINGS, + Feature.NUMERIC_UNDERSCORES, + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, + Feature.ASSIGNMENT_EXPRESSIONS, + Feature.RELAXED_DECORATORS, + Feature.POS_ONLY_ARGUMENTS, + Feature.UNPACKING_ON_FLOW, + Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PATTERN_MATCHING, + Feature.EXCEPT_STAR, + }, } diff --git a/src/black/nodes.py b/src/black/nodes.py index d18d4bde872..37b96a498d6 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -401,6 +401,10 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 elif p.type == syms.sliceop: return NO + elif p.type == syms.except_clause: + if t == token.STAR: + return NO + return SPACE diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 0ce6cf39111..1de54165513 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -118,7 +118,7 @@ try_stmt: ('try' ':' suite with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite # NB compile.c makes sure that the default except clause is last -except_clause: 'except' [test [(',' | 'as') test]] +except_clause: 'except' ['*'] [test [(',' | 'as') test]] suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT # Backward compatibility cruft to support: diff --git a/tests/data/pep_654.py b/tests/data/pep_654.py new file mode 100644 index 00000000000..387c0816f4b --- /dev/null +++ b/tests/data/pep_654.py @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/tests/data/pep_654_style.py b/tests/data/pep_654_style.py new file mode 100644 index 00000000000..568e5e3efa4 --- /dev/null +++ b/tests/data/pep_654_style.py @@ -0,0 +1,111 @@ +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e + +# output + +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* (Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/tests/test_black.py b/tests/test_black.py index 20cc9f7379f..f6663fa5797 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -794,6 +794,12 @@ def test_get_features_used(self) -> None: self.assertEqual( black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} ) + node = black.lib2to3_parse("try: pass\nexcept Something: pass") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass") + self.assertEqual(black.get_features_used(node), set()) + node = black.lib2to3_parse("try: pass\nexcept *Group: pass") + self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) def test_get_features_used_for_future_flags(self) -> None: for src, features in [ diff --git a/tests/test_format.py b/tests/test_format.py index fd5f596b6d5..1916146e84d 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -72,6 +72,11 @@ "parenthesized_context_managers", ] +PY311_CASES: List[str] = [ + "pep_654", + "pep_654_style", +] + PREVIEW_CASES: List[str] = [ # string processing "cantfit", @@ -227,6 +232,13 @@ def test_patma_invalid() -> None: exc_info.match("Cannot parse: 10:11") +@pytest.mark.parametrize("filename", PY311_CASES) +def test_python_311(filename: str) -> None: + source, expected = read_data(filename) + mode = black.Mode(target_versions={black.TargetVersion.PY311}) + assert_format(source, expected, mode, minimum_version=(3, 11)) + + def test_python_2_hint() -> None: with pytest.raises(black.parsing.InvalidInput) as exc_info: assert_format("print 'daylily'", "print 'daylily'") From 8ed3e3d07ea3e6d62e3e533e69f96a0ff148cd5d Mon Sep 17 00:00:00 2001 From: JiriKr <33967184+JiriKr@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:55:56 +0200 Subject: [PATCH 228/700] Updated Black Docker Hub link in docs (#3023) Fixes #3022 --- docs/usage_and_configuration/black_docker_image.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md index 0a458434871..8de566ea270 100644 --- a/docs/usage_and_configuration/black_docker_image.md +++ b/docs/usage_and_configuration/black_docker_image.md @@ -1,7 +1,7 @@ # Black Docker image -Official _Black_ Docker images are available on Docker Hub: -https://hub.docker.com/r/pyfound/black +Official _Black_ Docker images are available on +[Docker Hub](https://hub.docker.com/r/pyfound/black). _Black_ images with the following tags are available: From c6800e0c659eba5b22190bf6ae0ba563a4170661 Mon Sep 17 00:00:00 2001 From: Vadim Nikolaev Date: Thu, 28 Apr 2022 20:17:23 +0500 Subject: [PATCH 229/700] Fix strtobool function (#3025) * Fix strtobool function for vim plugin * Update CHANGES.md Co-authored-by: Cooper Lees --- CHANGES.md | 4 ++++ autoload/black.vim | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 566077b1dbc..4cea7fceaad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -62,6 +62,10 @@ +### Vim Plugin + +- Fixed strtobool function. It didn't parse true/on/false/off. (#3025) + ## 22.3.0 ### Preview style diff --git a/autoload/black.vim b/autoload/black.vim index 66c5b9c2841..6c381b431a3 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -5,9 +5,9 @@ import sys import vim def strtobool(text): - if text.lower() in ['y', 'yes', 't', 'true' 'on', '1']: + if text.lower() in ['y', 'yes', 't', 'true', 'on', '1']: return True - if text.lower() in ['n', 'no', 'f', 'false' 'off', '0']: + if text.lower() in ['n', 'no', 'f', 'false', 'off', '0']: return False raise ValueError(f"{text} is not convertable to boolean") From fb8dfdeec5fd76cc0c30f881d6fc75851139d80a Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 28 Apr 2022 10:27:16 -0600 Subject: [PATCH 230/700] Stop pinning lark-parser (#3041) - Latest version works more Test: `tox -e fuzz` --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 090dc522cad..258e6c5c203 100644 --- a/tox.ini +++ b/tox.ini @@ -55,8 +55,7 @@ skip_install = True deps = -r{toxinidir}/test_requirements.txt hypothesmith - lark-parser < 0.10.0 -; lark-parser's version is set due to a bug in hypothesis. Once it solved, that would be fixed. + lark-parser commands = pip install -e .[d] coverage erase From 9d5edd302003285b5280e3dd209d6299feafb70e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 11:12:23 -0600 Subject: [PATCH 231/700] Bump myst-parser from 0.16.1 to 0.17.2 in /docs (#3019) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 0.16.1 to 0.17.2. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v0.16.1...v0.17.2) --- updated-dependencies: - dependency-name: myst-parser 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 818415c6cf8..72cf09fb6e9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.16.1 +myst-parser==0.17.2 Sphinx==4.5.0 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 From c940f75d5b646777427aef1beb18a0d2c391f5e2 Mon Sep 17 00:00:00 2001 From: Naveen <172697+naveensrinivasan@users.noreply.github.com> Date: Tue, 3 May 2022 08:08:33 -0500 Subject: [PATCH 232/700] chore: Set permissions for GitHub actions (#3043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restrict the GitHub token permissions only to the required ones; this way, even if the attackers will succeed in compromising your workflow, they won’t be able to do much. - Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs [Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> --- .github/workflows/changelog.yml | 3 +++ .github/workflows/doc.yml | 3 +++ .github/workflows/docker.yml | 3 +++ .github/workflows/fuzz.yml | 3 +++ .github/workflows/pypi_upload.yml | 3 +++ .github/workflows/upload_binary.yml | 5 +++++ .github/workflows/uvloop_test.yml | 3 +++ 7 files changed, 23 insertions(+) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 3ffdb086493..b3e1f0b9024 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -4,6 +4,9 @@ on: pull_request: types: [opened, synchronize, labeled, unlabeled, reopened] +permissions: + contents: read + jobs: build: name: Changelog Entry Check diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 1ad4b3a7605..e2a0142cc65 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -2,6 +2,9 @@ name: Documentation Build on: [push, pull_request] +permissions: + contents: read + jobs: build: # We want to run on external PRs, but not on our own internal PRs as they'll be run diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b75ce2bb6f1..0a4848faad8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,6 +7,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: docker: if: github.repository == 'psf/black' diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 8fba67a5a01..d796fd50564 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -2,6 +2,9 @@ name: Fuzz on: [push, pull_request] +permissions: + contents: read + jobs: build: # We want to run on external PRs, but not on our own internal PRs as they'll be run diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 9d970592d98..ef524a8ece6 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: build: name: PyPI Upload diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index ed8d9fdd572..6bb1d23306b 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -4,8 +4,13 @@ on: release: types: [published] +permissions: + contents: read + jobs: build: + permissions: + contents: write # for actions/upload-release-asset to upload release asset runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml index 14b17d68424..bbc39935f89 100644 --- a/.github/workflows/uvloop_test.yml +++ b/.github/workflows/uvloop_test.yml @@ -11,6 +11,9 @@ on: - "docs/**" - "*.md" +permissions: + contents: read + jobs: build: # We want to run on external PRs, but not on our own internal PRs as they'll be run From 9ce100ba61b6738298d86818a3d0eee7b18bfed7 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 6 May 2022 07:06:27 -0700 Subject: [PATCH 233/700] Move imports of `ThreadPoolExecutor` into `reformat_many()`, allowing Black-in-the-browser (#3046) This is a slight perf win for use-cases that don't invoke `reformat_many()`, but more importantly to me today it means I can use Black in pyscript. --- src/black/__init__.py | 9 +++++++-- tests/test_black.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 3a1ce24f059..75321c3f35c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,7 +1,6 @@ import asyncio from json.decoder import JSONDecodeError import json -from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor from contextlib import contextmanager from datetime import datetime from enum import Enum @@ -17,6 +16,7 @@ import tokenize import traceback from typing import ( + TYPE_CHECKING, Any, Dict, Generator, @@ -77,6 +77,9 @@ from _black_version import version as __version__ +if TYPE_CHECKING: + from concurrent.futures import Executor + COMPILED = Path(__file__).suffix in (".pyd", ".so") # types @@ -767,6 +770,8 @@ def reformat_many( workers: Optional[int], ) -> None: """Reformat multiple files using a ProcessPoolExecutor.""" + from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor + executor: Executor loop = asyncio.get_event_loop() worker_count = workers if workers is not None else DEFAULT_WORKERS @@ -808,7 +813,7 @@ async def schedule_formatting( mode: Mode, report: "Report", loop: asyncio.AbstractEventLoop, - executor: Executor, + executor: "Executor", ) -> None: """Run formatting of `sources` in parallel using the provided `executor`. diff --git a/tests/test_black.py b/tests/test_black.py index f6663fa5797..74334d267a1 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -922,7 +922,7 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual("".join(err_lines), "") @event_loop() - @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError)) + @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError)) def test_works_in_mono_process_only_environment(self) -> None: with cache_dir() as workspace: for f in [ @@ -1683,7 +1683,7 @@ def test_cache_single_file_already_cached(self) -> None: def test_cache_multiple_files(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace, patch( - "black.ProcessPoolExecutor", new=ThreadPoolExecutor + "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor ): one = (workspace / "one.py").resolve() with one.open("w") as fobj: @@ -1792,7 +1792,7 @@ def test_write_cache_creates_directory_if_needed(self) -> None: def test_failed_formatting_does_not_get_cached(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace, patch( - "black.ProcessPoolExecutor", new=ThreadPoolExecutor + "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor ): failing = (workspace / "failing.py").resolve() with failing.open("w") as fobj: From 62c2b167bcf22683fc11add2f24a132d36e8fd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Sun, 8 May 2022 03:58:10 +0300 Subject: [PATCH 234/700] Docs: clarify fmt:on/off requirements (#2985) (#3048) --- docs/the_black_code_style/current_style.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index d54c7abaf5d..5085b0017d9 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -8,9 +8,10 @@ deliberately limited and rarely added. Previous formatting is taken into account 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 +_Black_ reformats entire files in place. It doesn't reformat lines that end with +`# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. +`# fmt: on/off` must be on the same level of indentation and in the same block, meaning +no unindents beyond the initial indentation level between them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a courtesy for straddling code. From 20d8ccb54253f8a66321f6708d53e2a05a54079b Mon Sep 17 00:00:00 2001 From: Iain Dorrington Date: Sun, 8 May 2022 05:34:28 +0100 Subject: [PATCH 235/700] Put closing quote on a separate line if docstring is too long (#3044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1632 Co-authored-by: Felix Hildén --- CHANGES.md | 1 + src/black/linegen.py | 26 +++++++++- src/black/mode.py | 1 + tests/data/docstring.py | 42 ++++++++++++++++ tests/data/docstring_preview.py | 89 +++++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/data/docstring_preview.py diff --git a/CHANGES.md b/CHANGES.md index 4cea7fceaad..8f43431c842 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ +- Fixed bug where docstrings with triple quotes could exceed max line length (#3044) - Remove redundant parentheses around awaited objects (#2991) - Parentheses around return annotations are now managed (#2990) - Remove unnecessary parentheses from `with` statements (#2926) diff --git a/src/black/linegen.py b/src/black/linegen.py index 91fdeef8f2f..ff54e05c4e6 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -305,9 +305,9 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote_len = 1 if docstring[1] != quote_char else 3 docstring = docstring[quote_len:-quote_len] docstring_started_empty = not docstring + indent = " " * 4 * self.current_line.depth if is_multiline_string(leaf): - indent = " " * 4 * self.current_line.depth docstring = fix_docstring(docstring, indent) else: docstring = docstring.strip() @@ -329,7 +329,29 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # We could enforce triple quotes at this point. quote = quote_char * quote_len - leaf.value = prefix + quote + docstring + quote + + if Preview.long_docstring_quotes_on_newline in self.mode: + # We need to find the length of the last line of the docstring + # to find if we can add the closing quotes to the line without + # exceeding the maximum line length. + # If docstring is one line, then we need to add the length + # of the indent, prefix, and starting quotes. Ending quote are + # handled later + lines = docstring.splitlines() + last_line_length = len(lines[-1]) if docstring else 0 + + if len(lines) == 1: + last_line_length += len(indent) + len(prefix) + quote_len + + # If adding closing quotes would cause the last line to exceed + # the maximum line length then put a line break before the + # closing quotes + if last_line_length + quote_len > self.mode.line_length: + leaf.value = prefix + quote + docstring + "\n" + indent + quote + else: + leaf.value = prefix + quote + docstring + quote + else: + leaf.value = prefix + quote + docstring + quote yield from self.visit_default(leaf) diff --git a/src/black/mode.py b/src/black/mode.py index 6bd4ce14421..a418e0eb665 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -147,6 +147,7 @@ class Preview(Enum): remove_redundant_parens = auto() one_element_subscript = auto() annotation_parens = auto() + long_docstring_quotes_on_newline = auto() class Deprecated(UserWarning): diff --git a/tests/data/docstring.py b/tests/data/docstring.py index 96bcf525b16..7153be468c1 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -188,6 +188,27 @@ def my_god_its_full_of_stars_2(): "I'm sorry Dave " +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit2(): + """long docstring................................................................. + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + # output class MyClass: @@ -375,3 +396,24 @@ def my_god_its_full_of_stars_1(): # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): "I'm sorry Dave" + + +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit2(): + """long docstring................................................................. + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" diff --git a/tests/data/docstring_preview.py b/tests/data/docstring_preview.py new file mode 100644 index 00000000000..2da4cd1acdb --- /dev/null +++ b/tests/data/docstring_preview.py @@ -0,0 +1,89 @@ +def docstring_almost_at_line_limit(): + """long docstring................................................................. + """ + + +def docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + """ + + +def mulitline_docstring_almost_at_line_limit(): + """long docstring................................................................. + + .................................................................................. + """ + + +def mulitline_docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def docstring_at_line_limit_with_prefix(): + f"""long docstring...............................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def multiline_docstring_at_line_limit_with_prefix(): + f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +# output + + +def docstring_almost_at_line_limit(): + """long docstring................................................................. + """ + + +def docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + """ + + +def mulitline_docstring_almost_at_line_limit(): + """long docstring................................................................. + + .................................................................................. + """ + + +def mulitline_docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def docstring_at_line_limit_with_prefix(): + f"""long docstring...............................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def multiline_docstring_at_line_limit_with_prefix(): + f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" diff --git a/tests/test_format.py b/tests/test_format.py index 1916146e84d..2f08d1f273d 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -91,6 +91,7 @@ "one_element_subscript", "remove_await_parens", "return_annotation_brackets", + "docstring_preview", ] SOURCES: List[str] = [ From fc2a16433e7da705793122dd0c66fcde83b305d5 Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Sun, 8 May 2022 22:27:40 +0300 Subject: [PATCH 236/700] Read simple data cases automatically (#3034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Hildén --- .../attribute_access_on_number_literals.py | 0 .../{ => simple_cases}/beginning_backslash.py | 0 tests/data/{ => simple_cases}/bracketmatch.py | 0 .../class_blank_parentheses.py | 0 .../class_methods_new_line.py | 0 tests/data/{ => simple_cases}/collections.py | 0 .../comment_after_escaped_newline.py | 0 tests/data/{ => simple_cases}/comments.py | 0 tests/data/{ => simple_cases}/comments2.py | 0 tests/data/{ => simple_cases}/comments3.py | 0 tests/data/{ => simple_cases}/comments4.py | 0 tests/data/{ => simple_cases}/comments5.py | 0 tests/data/{ => simple_cases}/comments6.py | 0 .../comments_non_breaking_space.py | 0 tests/data/{ => simple_cases}/composition.py | 0 .../composition_no_trailing_comma.py | 0 tests/data/{ => simple_cases}/docstring.py | 0 tests/data/{ => simple_cases}/empty_lines.py | 0 tests/data/{ => simple_cases}/expression.diff | 0 tests/data/{ => simple_cases}/expression.py | 0 tests/data/{ => simple_cases}/fmtonoff.py | 0 tests/data/{ => simple_cases}/fmtonoff2.py | 0 tests/data/{ => simple_cases}/fmtonoff3.py | 0 tests/data/{ => simple_cases}/fmtonoff4.py | 0 tests/data/{ => simple_cases}/fmtskip.py | 0 tests/data/{ => simple_cases}/fmtskip2.py | 0 tests/data/{ => simple_cases}/fmtskip3.py | 0 tests/data/{ => simple_cases}/fmtskip4.py | 0 tests/data/{ => simple_cases}/fmtskip5.py | 0 tests/data/{ => simple_cases}/fmtskip6.py | 0 tests/data/{ => simple_cases}/fmtskip7.py | 0 tests/data/{ => simple_cases}/fstring.py | 0 tests/data/{ => simple_cases}/function.py | 0 tests/data/{ => simple_cases}/function2.py | 0 .../function_trailing_comma.py | 0 .../data/{ => simple_cases}/import_spacing.py | 0 .../{ => simple_cases}/power_op_spacing.py | 0 .../data/{ => simple_cases}/remove_parens.py | 0 tests/data/{ => simple_cases}/slices.py | 0 .../{ => simple_cases}/string_prefixes.py | 0 tests/data/{ => simple_cases}/torture.py | 0 .../trailing_comma_optional_parens1.py | 0 .../trailing_comma_optional_parens2.py | 0 .../trailing_comma_optional_parens3.py | 0 .../tricky_unicode_symbols.py | 0 tests/data/{ => simple_cases}/tupleassign.py | 0 tests/test_black.py | 26 +++++----- tests/test_format.py | 51 +------------------ tests/util.py | 11 +++- 49 files changed, 25 insertions(+), 63 deletions(-) rename tests/data/{ => simple_cases}/attribute_access_on_number_literals.py (100%) rename tests/data/{ => simple_cases}/beginning_backslash.py (100%) rename tests/data/{ => simple_cases}/bracketmatch.py (100%) rename tests/data/{ => simple_cases}/class_blank_parentheses.py (100%) rename tests/data/{ => simple_cases}/class_methods_new_line.py (100%) rename tests/data/{ => simple_cases}/collections.py (100%) rename tests/data/{ => simple_cases}/comment_after_escaped_newline.py (100%) rename tests/data/{ => simple_cases}/comments.py (100%) rename tests/data/{ => simple_cases}/comments2.py (100%) rename tests/data/{ => simple_cases}/comments3.py (100%) rename tests/data/{ => simple_cases}/comments4.py (100%) rename tests/data/{ => simple_cases}/comments5.py (100%) rename tests/data/{ => simple_cases}/comments6.py (100%) rename tests/data/{ => simple_cases}/comments_non_breaking_space.py (100%) rename tests/data/{ => simple_cases}/composition.py (100%) rename tests/data/{ => simple_cases}/composition_no_trailing_comma.py (100%) rename tests/data/{ => simple_cases}/docstring.py (100%) rename tests/data/{ => simple_cases}/empty_lines.py (100%) rename tests/data/{ => simple_cases}/expression.diff (100%) rename tests/data/{ => simple_cases}/expression.py (100%) rename tests/data/{ => simple_cases}/fmtonoff.py (100%) rename tests/data/{ => simple_cases}/fmtonoff2.py (100%) rename tests/data/{ => simple_cases}/fmtonoff3.py (100%) rename tests/data/{ => simple_cases}/fmtonoff4.py (100%) rename tests/data/{ => simple_cases}/fmtskip.py (100%) rename tests/data/{ => simple_cases}/fmtskip2.py (100%) rename tests/data/{ => simple_cases}/fmtskip3.py (100%) rename tests/data/{ => simple_cases}/fmtskip4.py (100%) rename tests/data/{ => simple_cases}/fmtskip5.py (100%) rename tests/data/{ => simple_cases}/fmtskip6.py (100%) rename tests/data/{ => simple_cases}/fmtskip7.py (100%) rename tests/data/{ => simple_cases}/fstring.py (100%) rename tests/data/{ => simple_cases}/function.py (100%) rename tests/data/{ => simple_cases}/function2.py (100%) rename tests/data/{ => simple_cases}/function_trailing_comma.py (100%) rename tests/data/{ => simple_cases}/import_spacing.py (100%) rename tests/data/{ => simple_cases}/power_op_spacing.py (100%) rename tests/data/{ => simple_cases}/remove_parens.py (100%) rename tests/data/{ => simple_cases}/slices.py (100%) rename tests/data/{ => simple_cases}/string_prefixes.py (100%) rename tests/data/{ => simple_cases}/torture.py (100%) rename tests/data/{ => simple_cases}/trailing_comma_optional_parens1.py (100%) rename tests/data/{ => simple_cases}/trailing_comma_optional_parens2.py (100%) rename tests/data/{ => simple_cases}/trailing_comma_optional_parens3.py (100%) rename tests/data/{ => simple_cases}/tricky_unicode_symbols.py (100%) rename tests/data/{ => simple_cases}/tupleassign.py (100%) diff --git a/tests/data/attribute_access_on_number_literals.py b/tests/data/simple_cases/attribute_access_on_number_literals.py similarity index 100% rename from tests/data/attribute_access_on_number_literals.py rename to tests/data/simple_cases/attribute_access_on_number_literals.py diff --git a/tests/data/beginning_backslash.py b/tests/data/simple_cases/beginning_backslash.py similarity index 100% rename from tests/data/beginning_backslash.py rename to tests/data/simple_cases/beginning_backslash.py diff --git a/tests/data/bracketmatch.py b/tests/data/simple_cases/bracketmatch.py similarity index 100% rename from tests/data/bracketmatch.py rename to tests/data/simple_cases/bracketmatch.py diff --git a/tests/data/class_blank_parentheses.py b/tests/data/simple_cases/class_blank_parentheses.py similarity index 100% rename from tests/data/class_blank_parentheses.py rename to tests/data/simple_cases/class_blank_parentheses.py diff --git a/tests/data/class_methods_new_line.py b/tests/data/simple_cases/class_methods_new_line.py similarity index 100% rename from tests/data/class_methods_new_line.py rename to tests/data/simple_cases/class_methods_new_line.py diff --git a/tests/data/collections.py b/tests/data/simple_cases/collections.py similarity index 100% rename from tests/data/collections.py rename to tests/data/simple_cases/collections.py diff --git a/tests/data/comment_after_escaped_newline.py b/tests/data/simple_cases/comment_after_escaped_newline.py similarity index 100% rename from tests/data/comment_after_escaped_newline.py rename to tests/data/simple_cases/comment_after_escaped_newline.py diff --git a/tests/data/comments.py b/tests/data/simple_cases/comments.py similarity index 100% rename from tests/data/comments.py rename to tests/data/simple_cases/comments.py diff --git a/tests/data/comments2.py b/tests/data/simple_cases/comments2.py similarity index 100% rename from tests/data/comments2.py rename to tests/data/simple_cases/comments2.py diff --git a/tests/data/comments3.py b/tests/data/simple_cases/comments3.py similarity index 100% rename from tests/data/comments3.py rename to tests/data/simple_cases/comments3.py diff --git a/tests/data/comments4.py b/tests/data/simple_cases/comments4.py similarity index 100% rename from tests/data/comments4.py rename to tests/data/simple_cases/comments4.py diff --git a/tests/data/comments5.py b/tests/data/simple_cases/comments5.py similarity index 100% rename from tests/data/comments5.py rename to tests/data/simple_cases/comments5.py diff --git a/tests/data/comments6.py b/tests/data/simple_cases/comments6.py similarity index 100% rename from tests/data/comments6.py rename to tests/data/simple_cases/comments6.py diff --git a/tests/data/comments_non_breaking_space.py b/tests/data/simple_cases/comments_non_breaking_space.py similarity index 100% rename from tests/data/comments_non_breaking_space.py rename to tests/data/simple_cases/comments_non_breaking_space.py diff --git a/tests/data/composition.py b/tests/data/simple_cases/composition.py similarity index 100% rename from tests/data/composition.py rename to tests/data/simple_cases/composition.py diff --git a/tests/data/composition_no_trailing_comma.py b/tests/data/simple_cases/composition_no_trailing_comma.py similarity index 100% rename from tests/data/composition_no_trailing_comma.py rename to tests/data/simple_cases/composition_no_trailing_comma.py diff --git a/tests/data/docstring.py b/tests/data/simple_cases/docstring.py similarity index 100% rename from tests/data/docstring.py rename to tests/data/simple_cases/docstring.py diff --git a/tests/data/empty_lines.py b/tests/data/simple_cases/empty_lines.py similarity index 100% rename from tests/data/empty_lines.py rename to tests/data/simple_cases/empty_lines.py diff --git a/tests/data/expression.diff b/tests/data/simple_cases/expression.diff similarity index 100% rename from tests/data/expression.diff rename to tests/data/simple_cases/expression.diff diff --git a/tests/data/expression.py b/tests/data/simple_cases/expression.py similarity index 100% rename from tests/data/expression.py rename to tests/data/simple_cases/expression.py diff --git a/tests/data/fmtonoff.py b/tests/data/simple_cases/fmtonoff.py similarity index 100% rename from tests/data/fmtonoff.py rename to tests/data/simple_cases/fmtonoff.py diff --git a/tests/data/fmtonoff2.py b/tests/data/simple_cases/fmtonoff2.py similarity index 100% rename from tests/data/fmtonoff2.py rename to tests/data/simple_cases/fmtonoff2.py diff --git a/tests/data/fmtonoff3.py b/tests/data/simple_cases/fmtonoff3.py similarity index 100% rename from tests/data/fmtonoff3.py rename to tests/data/simple_cases/fmtonoff3.py diff --git a/tests/data/fmtonoff4.py b/tests/data/simple_cases/fmtonoff4.py similarity index 100% rename from tests/data/fmtonoff4.py rename to tests/data/simple_cases/fmtonoff4.py diff --git a/tests/data/fmtskip.py b/tests/data/simple_cases/fmtskip.py similarity index 100% rename from tests/data/fmtskip.py rename to tests/data/simple_cases/fmtskip.py diff --git a/tests/data/fmtskip2.py b/tests/data/simple_cases/fmtskip2.py similarity index 100% rename from tests/data/fmtskip2.py rename to tests/data/simple_cases/fmtskip2.py diff --git a/tests/data/fmtskip3.py b/tests/data/simple_cases/fmtskip3.py similarity index 100% rename from tests/data/fmtskip3.py rename to tests/data/simple_cases/fmtskip3.py diff --git a/tests/data/fmtskip4.py b/tests/data/simple_cases/fmtskip4.py similarity index 100% rename from tests/data/fmtskip4.py rename to tests/data/simple_cases/fmtskip4.py diff --git a/tests/data/fmtskip5.py b/tests/data/simple_cases/fmtskip5.py similarity index 100% rename from tests/data/fmtskip5.py rename to tests/data/simple_cases/fmtskip5.py diff --git a/tests/data/fmtskip6.py b/tests/data/simple_cases/fmtskip6.py similarity index 100% rename from tests/data/fmtskip6.py rename to tests/data/simple_cases/fmtskip6.py diff --git a/tests/data/fmtskip7.py b/tests/data/simple_cases/fmtskip7.py similarity index 100% rename from tests/data/fmtskip7.py rename to tests/data/simple_cases/fmtskip7.py diff --git a/tests/data/fstring.py b/tests/data/simple_cases/fstring.py similarity index 100% rename from tests/data/fstring.py rename to tests/data/simple_cases/fstring.py diff --git a/tests/data/function.py b/tests/data/simple_cases/function.py similarity index 100% rename from tests/data/function.py rename to tests/data/simple_cases/function.py diff --git a/tests/data/function2.py b/tests/data/simple_cases/function2.py similarity index 100% rename from tests/data/function2.py rename to tests/data/simple_cases/function2.py diff --git a/tests/data/function_trailing_comma.py b/tests/data/simple_cases/function_trailing_comma.py similarity index 100% rename from tests/data/function_trailing_comma.py rename to tests/data/simple_cases/function_trailing_comma.py diff --git a/tests/data/import_spacing.py b/tests/data/simple_cases/import_spacing.py similarity index 100% rename from tests/data/import_spacing.py rename to tests/data/simple_cases/import_spacing.py diff --git a/tests/data/power_op_spacing.py b/tests/data/simple_cases/power_op_spacing.py similarity index 100% rename from tests/data/power_op_spacing.py rename to tests/data/simple_cases/power_op_spacing.py diff --git a/tests/data/remove_parens.py b/tests/data/simple_cases/remove_parens.py similarity index 100% rename from tests/data/remove_parens.py rename to tests/data/simple_cases/remove_parens.py diff --git a/tests/data/slices.py b/tests/data/simple_cases/slices.py similarity index 100% rename from tests/data/slices.py rename to tests/data/simple_cases/slices.py diff --git a/tests/data/string_prefixes.py b/tests/data/simple_cases/string_prefixes.py similarity index 100% rename from tests/data/string_prefixes.py rename to tests/data/simple_cases/string_prefixes.py diff --git a/tests/data/torture.py b/tests/data/simple_cases/torture.py similarity index 100% rename from tests/data/torture.py rename to tests/data/simple_cases/torture.py diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/simple_cases/trailing_comma_optional_parens1.py similarity index 100% rename from tests/data/trailing_comma_optional_parens1.py rename to tests/data/simple_cases/trailing_comma_optional_parens1.py diff --git a/tests/data/trailing_comma_optional_parens2.py b/tests/data/simple_cases/trailing_comma_optional_parens2.py similarity index 100% rename from tests/data/trailing_comma_optional_parens2.py rename to tests/data/simple_cases/trailing_comma_optional_parens2.py diff --git a/tests/data/trailing_comma_optional_parens3.py b/tests/data/simple_cases/trailing_comma_optional_parens3.py similarity index 100% rename from tests/data/trailing_comma_optional_parens3.py rename to tests/data/simple_cases/trailing_comma_optional_parens3.py diff --git a/tests/data/tricky_unicode_symbols.py b/tests/data/simple_cases/tricky_unicode_symbols.py similarity index 100% rename from tests/data/tricky_unicode_symbols.py rename to tests/data/simple_cases/tricky_unicode_symbols.py diff --git a/tests/data/tupleassign.py b/tests/data/simple_cases/tupleassign.py similarity index 100% rename from tests/data/tupleassign.py rename to tests/data/simple_cases/tupleassign.py diff --git a/tests/test_black.py b/tests/test_black.py index 74334d267a1..281019a0bfa 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -179,8 +179,8 @@ def test_piping_diff(self) -> None: r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d " r"\+\d\d\d\d" ) - source, _ = read_data("expression.py") - expected, _ = read_data("expression.diff") + source, _ = read_data("simple_cases/expression.py") + expected, _ = read_data("simple_cases/expression.diff") args = [ "-", "--fast", @@ -197,7 +197,7 @@ def test_piping_diff(self) -> None: self.assertEqual(expected, actual) def test_piping_diff_with_color(self) -> None: - source, _ = read_data("expression.py") + source, _ = read_data("simple_cases/expression.py") args = [ "-", "--fast", @@ -241,7 +241,7 @@ def test_pep_572_version_detection(self) -> None: self.assertIn(black.TargetVersion.PY38, versions) def test_expression_ff(self) -> None: - source, expected = read_data("expression") + source, expected = read_data("simple_cases/expression.py") tmp_file = Path(black.dump_to_file(source)) try: self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES)) @@ -255,8 +255,8 @@ def test_expression_ff(self) -> None: black.assert_stable(source, actual, DEFAULT_MODE) def test_expression_diff(self) -> None: - source, _ = read_data("expression.py") - expected, _ = read_data("expression.diff") + source, _ = read_data("simple_cases/expression.py") + expected, _ = read_data("simple_cases/expression.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " @@ -281,8 +281,8 @@ def test_expression_diff(self) -> None: self.assertEqual(expected, actual, msg) def test_expression_diff_with_color(self) -> None: - source, _ = read_data("expression.py") - expected, _ = read_data("expression.diff") + source, _ = read_data("simple_cases/expression.py") + expected, _ = read_data("simple_cases/expression.diff") tmp_file = Path(black.dump_to_file(source)) try: result = BlackRunner().invoke( @@ -320,7 +320,7 @@ def test_string_quotes(self) -> None: black.assert_stable(source, not_normalized, mode=mode) def test_skip_magic_trailing_comma(self) -> None: - source, _ = read_data("expression.py") + source, _ = read_data("simple_cases/expression.py") expected, _ = read_data("expression_skip_magic_trailing_comma.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( @@ -755,7 +755,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) node = black.lib2to3_parse("123456\n") self.assertEqual(black.get_features_used(node), set()) - source, expected = read_data("function") + source, expected = read_data("simple_cases/function.py") node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, @@ -765,7 +765,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), expected_features) node = black.lib2to3_parse(expected) self.assertEqual(black.get_features_used(node), expected_features) - source, expected = read_data("expression") + source, expected = read_data("simple_cases/expression.py") node = black.lib2to3_parse(source) self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse(expected) @@ -939,7 +939,7 @@ def test_check_diff_use_together(self) -> None: src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) # Files which will not be reformatted. - src2 = (THIS_DIR / "data" / "composition.py").resolve() + src2 = (THIS_DIR / "data" / "simple_cases" / "composition.py").resolve() self.invokeBlack([str(src2), "--diff", "--check"]) # Multi file command. self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) @@ -1168,7 +1168,7 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None: report = MagicMock() # Even with an existing file, since we are forcing stdin, black # should output to stdout and not modify the file inplace - p = Path(str(THIS_DIR / "data/collections.py")) + p = THIS_DIR / "data" / "simple_cases" / "collections.py" # Make sure is_file actually returns True self.assertTrue(p.is_file()) path = Path(f"__BLACK_STDIN_FILENAME__{p}") diff --git a/tests/test_format.py b/tests/test_format.py index 2f08d1f273d..003f5bbe188 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -12,56 +12,9 @@ assert_format, dump_to_stderr, read_data, + all_data_cases, ) -SIMPLE_CASES: List[str] = [ - "attribute_access_on_number_literals", - "beginning_backslash", - "bracketmatch", - "class_blank_parentheses", - "class_methods_new_line", - "collections", - "comments", - "comments2", - "comments3", - "comments4", - "comments5", - "comments6", - "comments_non_breaking_space", - "comment_after_escaped_newline", - "composition", - "composition_no_trailing_comma", - "docstring", - "empty_lines", - "expression", - "fmtonoff", - "fmtonoff2", - "fmtonoff3", - "fmtonoff4", - "fmtskip", - "fmtskip2", - "fmtskip3", - "fmtskip4", - "fmtskip5", - "fmtskip6", - "fmtskip7", - "fstring", - "function", - "function2", - "function_trailing_comma", - "import_spacing", - "power_op_spacing", - "remove_parens", - "slices", - "string_prefixes", - "torture", - "trailing_comma_optional_parens1", - "trailing_comma_optional_parens2", - "trailing_comma_optional_parens3", - "tricky_unicode_symbols", - "tupleassign", -] - PY310_CASES: List[str] = [ "starred_for_target", "pattern_matching_simple", @@ -147,7 +100,7 @@ def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None: assert_format(source, expected, mode, fast=False) -@pytest.mark.parametrize("filename", SIMPLE_CASES) +@pytest.mark.parametrize("filename", all_data_cases("simple_cases")) def test_simple_format(filename: str) -> None: check_file(filename, DEFAULT_MODE) diff --git a/tests/util.py b/tests/util.py index 8755111f7c5..1d76681dbea 100644 --- a/tests/util.py +++ b/tests/util.py @@ -90,12 +90,21 @@ def assertFormatEqual(self, expected: str, actual: str) -> None: _assert_format_equal(expected, actual) +def all_data_cases(dir_name: str, data: bool = True) -> List[str]: + base_dir = DATA_DIR if data else PROJECT_ROOT + cases_dir = base_dir / dir_name + assert cases_dir.is_dir() + return [f"{dir_name}/{case_path.stem}" for case_path in cases_dir.iterdir()] + + def read_data(name: str, data: bool = True) -> Tuple[str, str]: """read_data('test_name') -> 'input', 'output'""" if not name.endswith((".py", ".pyi", ".out", ".diff")): name += ".py" base_dir = DATA_DIR if data else PROJECT_ROOT - return read_data_from_file(base_dir / name) + case_path = base_dir / name + assert case_path.is_file(), f"{case_path} is not a file." + return read_data_from_file(case_path) def read_data_from_file(file_name: Path) -> Tuple[str, str]: From 5d5b7316db2f001e079ba980b0a72681caac4912 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 22:08:56 -0400 Subject: [PATCH 237/700] Bump docker/setup-qemu-action from 1 to 2 (#3056) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0a4848faad8..6bd13ddb313 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 From ba21a8569977d279f14251254d57d64d47979436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 22:11:29 -0400 Subject: [PATCH 238/700] Bump docker/build-push-action from 2 to 3 (#3057) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2 to 3. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6bd13ddb313..60a9b77f75d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,7 +36,7 @@ jobs: latest_non_release)" >> $GITHUB_ENV - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 @@ -45,7 +45,7 @@ jobs: - name: Build and push latest_release tag if: ${{ github.event_name == 'release' && github.event.action == 'published' }} - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 From 4af87d8a43e2fd17045234d646dc59bfc8d77af4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 22:13:12 -0400 Subject: [PATCH 239/700] Bump docker/login-action from 1 to 2 (#3059) Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 2. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v1...v2) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 60a9b77f75d..dfde069ee4a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,7 +25,7 @@ jobs: uses: docker/setup-buildx-action@v1 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 7f033136ac5e0e5bf6cf322dd60b4a92050eedc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 22:22:01 -0400 Subject: [PATCH 240/700] Bump docker/setup-buildx-action from 1 to 2 (#3058) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 2. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v2) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dfde069ee4a..a3106d04aae 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 From 2893c42176903c8b6c28c46ff9e046861328b6a8 Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Wed, 18 May 2022 22:11:37 +0300 Subject: [PATCH 241/700] Remove hard coded test cases (#3062) --- .../pep_572_do_not_remove_parens.py | 0 .../{ => jupyter}/non_python_notebook.ipynb | 0 .../notebook_empty_metadata.ipynb | 0 .../notebook_no_trailing_newline.ipynb | 0 .../notebook_trailing_newline.ipynb | 0 .../notebook_which_cant_be_parsed.ipynb | 0 .../notebook_without_changes.ipynb | 0 .../async_as_identifier.py | 0 .../data/{ => miscellaneous}/blackd_diff.diff | 0 tests/data/{ => miscellaneous}/blackd_diff.py | 0 .../{ => miscellaneous}/debug_visitor.out | 0 .../data/{ => miscellaneous}/debug_visitor.py | 0 tests/data/{ => miscellaneous}/decorators.py | 0 .../docstring_no_string_normalization.py | 0 .../expression_skip_magic_trailing_comma.diff | 0 tests/data/{ => miscellaneous}/force_py36.py | 0 tests/data/{ => miscellaneous}/force_pyi.py | 0 .../long_strings_flag_disabled.py | 0 .../missing_final_newline.diff | 0 .../missing_final_newline.py | 0 .../pattern_matching_invalid.py | 0 .../{ => miscellaneous}/power_op_newline.py | 0 .../{ => miscellaneous}/python2_detection.py | 0 .../data/{ => miscellaneous}/string_quotes.py | 0 tests/data/{ => miscellaneous}/stub.pyi | 0 tests/data/{ => preview}/cantfit.py | 0 tests/data/{ => preview}/comments7.py | 0 tests/data/{ => preview}/comments8.py | 0 tests/data/{ => preview}/docstring_preview.py | 0 tests/data/{ => preview}/long_strings.py | 0 .../{ => preview}/long_strings__edge_case.py | 0 .../{ => preview}/long_strings__regression.py | 0 .../{ => preview}/one_element_subscript.py | 0 .../data/{ => preview}/percent_precedence.py | 0 .../data/{ => preview}/remove_await_parens.py | 0 .../{ => preview}/remove_except_parens.py | 0 .../data/{ => preview}/remove_for_brackets.py | 0 .../return_annotation_brackets.py | 0 .../{ => preview_39}/remove_with_brackets.py | 0 .../parenthesized_context_managers.py | 0 .../{ => py_310}/pattern_matching_complex.py | 0 .../{ => py_310}/pattern_matching_extras.py | 0 .../{ => py_310}/pattern_matching_generic.py | 0 .../{ => py_310}/pattern_matching_simple.py | 0 .../{ => py_310}/pattern_matching_style.py | 0 tests/data/{ => py_310}/pep_572_py310.py | 0 tests/data/{ => py_310}/starred_for_target.py | 0 tests/data/{ => py_311}/pep_654.py | 0 tests/data/{ => py_311}/pep_654_style.py | 0 tests/data/{ => py_36}/numeric_literals.py | 0 .../numeric_literals_skip_underscores.py | 0 tests/data/{ => py_37}/python37.py | 0 tests/data/{ => py_38}/pep_570.py | 0 tests/data/{ => py_38}/pep_572.py | 0 .../data/{ => py_38}/pep_572_remove_parens.py | 0 tests/data/{ => py_38}/python38.py | 0 tests/data/{ => py_39}/pep_572_py39.py | 0 tests/data/{ => py_39}/python39.py | 0 tests/test_black.py | 72 ++++---- tests/test_blackd.py | 6 +- tests/test_format.py | 156 ++++++------------ tests/test_ipynb.py | 37 ++--- tests/test_no_ipynb.py | 7 +- tests/util.py | 46 ++++-- 64 files changed, 146 insertions(+), 178 deletions(-) rename tests/data/{ => fast}/pep_572_do_not_remove_parens.py (100%) rename tests/data/{ => jupyter}/non_python_notebook.ipynb (100%) rename tests/data/{ => jupyter}/notebook_empty_metadata.ipynb (100%) rename tests/data/{ => jupyter}/notebook_no_trailing_newline.ipynb (100%) rename tests/data/{ => jupyter}/notebook_trailing_newline.ipynb (100%) rename tests/data/{ => jupyter}/notebook_which_cant_be_parsed.ipynb (100%) rename tests/data/{ => jupyter}/notebook_without_changes.ipynb (100%) rename tests/data/{ => miscellaneous}/async_as_identifier.py (100%) rename tests/data/{ => miscellaneous}/blackd_diff.diff (100%) rename tests/data/{ => miscellaneous}/blackd_diff.py (100%) rename tests/data/{ => miscellaneous}/debug_visitor.out (100%) rename tests/data/{ => miscellaneous}/debug_visitor.py (100%) rename tests/data/{ => miscellaneous}/decorators.py (100%) rename tests/data/{ => miscellaneous}/docstring_no_string_normalization.py (100%) rename tests/data/{ => miscellaneous}/expression_skip_magic_trailing_comma.diff (100%) rename tests/data/{ => miscellaneous}/force_py36.py (100%) rename tests/data/{ => miscellaneous}/force_pyi.py (100%) rename tests/data/{ => miscellaneous}/long_strings_flag_disabled.py (100%) rename tests/data/{ => miscellaneous}/missing_final_newline.diff (100%) rename tests/data/{ => miscellaneous}/missing_final_newline.py (100%) rename tests/data/{ => miscellaneous}/pattern_matching_invalid.py (100%) rename tests/data/{ => miscellaneous}/power_op_newline.py (100%) rename tests/data/{ => miscellaneous}/python2_detection.py (100%) rename tests/data/{ => miscellaneous}/string_quotes.py (100%) rename tests/data/{ => miscellaneous}/stub.pyi (100%) rename tests/data/{ => preview}/cantfit.py (100%) rename tests/data/{ => preview}/comments7.py (100%) rename tests/data/{ => preview}/comments8.py (100%) rename tests/data/{ => preview}/docstring_preview.py (100%) rename tests/data/{ => preview}/long_strings.py (100%) rename tests/data/{ => preview}/long_strings__edge_case.py (100%) rename tests/data/{ => preview}/long_strings__regression.py (100%) rename tests/data/{ => preview}/one_element_subscript.py (100%) rename tests/data/{ => preview}/percent_precedence.py (100%) rename tests/data/{ => preview}/remove_await_parens.py (100%) rename tests/data/{ => preview}/remove_except_parens.py (100%) rename tests/data/{ => preview}/remove_for_brackets.py (100%) rename tests/data/{ => preview}/return_annotation_brackets.py (100%) rename tests/data/{ => preview_39}/remove_with_brackets.py (100%) rename tests/data/{ => py_310}/parenthesized_context_managers.py (100%) rename tests/data/{ => py_310}/pattern_matching_complex.py (100%) rename tests/data/{ => py_310}/pattern_matching_extras.py (100%) rename tests/data/{ => py_310}/pattern_matching_generic.py (100%) rename tests/data/{ => py_310}/pattern_matching_simple.py (100%) rename tests/data/{ => py_310}/pattern_matching_style.py (100%) rename tests/data/{ => py_310}/pep_572_py310.py (100%) rename tests/data/{ => py_310}/starred_for_target.py (100%) rename tests/data/{ => py_311}/pep_654.py (100%) rename tests/data/{ => py_311}/pep_654_style.py (100%) rename tests/data/{ => py_36}/numeric_literals.py (100%) rename tests/data/{ => py_36}/numeric_literals_skip_underscores.py (100%) rename tests/data/{ => py_37}/python37.py (100%) rename tests/data/{ => py_38}/pep_570.py (100%) rename tests/data/{ => py_38}/pep_572.py (100%) rename tests/data/{ => py_38}/pep_572_remove_parens.py (100%) rename tests/data/{ => py_38}/python38.py (100%) rename tests/data/{ => py_39}/pep_572_py39.py (100%) rename tests/data/{ => py_39}/python39.py (100%) diff --git a/tests/data/pep_572_do_not_remove_parens.py b/tests/data/fast/pep_572_do_not_remove_parens.py similarity index 100% rename from tests/data/pep_572_do_not_remove_parens.py rename to tests/data/fast/pep_572_do_not_remove_parens.py diff --git a/tests/data/non_python_notebook.ipynb b/tests/data/jupyter/non_python_notebook.ipynb similarity index 100% rename from tests/data/non_python_notebook.ipynb rename to tests/data/jupyter/non_python_notebook.ipynb diff --git a/tests/data/notebook_empty_metadata.ipynb b/tests/data/jupyter/notebook_empty_metadata.ipynb similarity index 100% rename from tests/data/notebook_empty_metadata.ipynb rename to tests/data/jupyter/notebook_empty_metadata.ipynb diff --git a/tests/data/notebook_no_trailing_newline.ipynb b/tests/data/jupyter/notebook_no_trailing_newline.ipynb similarity index 100% rename from tests/data/notebook_no_trailing_newline.ipynb rename to tests/data/jupyter/notebook_no_trailing_newline.ipynb diff --git a/tests/data/notebook_trailing_newline.ipynb b/tests/data/jupyter/notebook_trailing_newline.ipynb similarity index 100% rename from tests/data/notebook_trailing_newline.ipynb rename to tests/data/jupyter/notebook_trailing_newline.ipynb diff --git a/tests/data/notebook_which_cant_be_parsed.ipynb b/tests/data/jupyter/notebook_which_cant_be_parsed.ipynb similarity index 100% rename from tests/data/notebook_which_cant_be_parsed.ipynb rename to tests/data/jupyter/notebook_which_cant_be_parsed.ipynb diff --git a/tests/data/notebook_without_changes.ipynb b/tests/data/jupyter/notebook_without_changes.ipynb similarity index 100% rename from tests/data/notebook_without_changes.ipynb rename to tests/data/jupyter/notebook_without_changes.ipynb diff --git a/tests/data/async_as_identifier.py b/tests/data/miscellaneous/async_as_identifier.py similarity index 100% rename from tests/data/async_as_identifier.py rename to tests/data/miscellaneous/async_as_identifier.py diff --git a/tests/data/blackd_diff.diff b/tests/data/miscellaneous/blackd_diff.diff similarity index 100% rename from tests/data/blackd_diff.diff rename to tests/data/miscellaneous/blackd_diff.diff diff --git a/tests/data/blackd_diff.py b/tests/data/miscellaneous/blackd_diff.py similarity index 100% rename from tests/data/blackd_diff.py rename to tests/data/miscellaneous/blackd_diff.py diff --git a/tests/data/debug_visitor.out b/tests/data/miscellaneous/debug_visitor.out similarity index 100% rename from tests/data/debug_visitor.out rename to tests/data/miscellaneous/debug_visitor.out diff --git a/tests/data/debug_visitor.py b/tests/data/miscellaneous/debug_visitor.py similarity index 100% rename from tests/data/debug_visitor.py rename to tests/data/miscellaneous/debug_visitor.py diff --git a/tests/data/decorators.py b/tests/data/miscellaneous/decorators.py similarity index 100% rename from tests/data/decorators.py rename to tests/data/miscellaneous/decorators.py diff --git a/tests/data/docstring_no_string_normalization.py b/tests/data/miscellaneous/docstring_no_string_normalization.py similarity index 100% rename from tests/data/docstring_no_string_normalization.py rename to tests/data/miscellaneous/docstring_no_string_normalization.py diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff similarity index 100% rename from tests/data/expression_skip_magic_trailing_comma.diff rename to tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff diff --git a/tests/data/force_py36.py b/tests/data/miscellaneous/force_py36.py similarity index 100% rename from tests/data/force_py36.py rename to tests/data/miscellaneous/force_py36.py diff --git a/tests/data/force_pyi.py b/tests/data/miscellaneous/force_pyi.py similarity index 100% rename from tests/data/force_pyi.py rename to tests/data/miscellaneous/force_pyi.py diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/miscellaneous/long_strings_flag_disabled.py similarity index 100% rename from tests/data/long_strings_flag_disabled.py rename to tests/data/miscellaneous/long_strings_flag_disabled.py diff --git a/tests/data/missing_final_newline.diff b/tests/data/miscellaneous/missing_final_newline.diff similarity index 100% rename from tests/data/missing_final_newline.diff rename to tests/data/miscellaneous/missing_final_newline.diff diff --git a/tests/data/missing_final_newline.py b/tests/data/miscellaneous/missing_final_newline.py similarity index 100% rename from tests/data/missing_final_newline.py rename to tests/data/miscellaneous/missing_final_newline.py diff --git a/tests/data/pattern_matching_invalid.py b/tests/data/miscellaneous/pattern_matching_invalid.py similarity index 100% rename from tests/data/pattern_matching_invalid.py rename to tests/data/miscellaneous/pattern_matching_invalid.py diff --git a/tests/data/power_op_newline.py b/tests/data/miscellaneous/power_op_newline.py similarity index 100% rename from tests/data/power_op_newline.py rename to tests/data/miscellaneous/power_op_newline.py diff --git a/tests/data/python2_detection.py b/tests/data/miscellaneous/python2_detection.py similarity index 100% rename from tests/data/python2_detection.py rename to tests/data/miscellaneous/python2_detection.py diff --git a/tests/data/string_quotes.py b/tests/data/miscellaneous/string_quotes.py similarity index 100% rename from tests/data/string_quotes.py rename to tests/data/miscellaneous/string_quotes.py diff --git a/tests/data/stub.pyi b/tests/data/miscellaneous/stub.pyi similarity index 100% rename from tests/data/stub.pyi rename to tests/data/miscellaneous/stub.pyi diff --git a/tests/data/cantfit.py b/tests/data/preview/cantfit.py similarity index 100% rename from tests/data/cantfit.py rename to tests/data/preview/cantfit.py diff --git a/tests/data/comments7.py b/tests/data/preview/comments7.py similarity index 100% rename from tests/data/comments7.py rename to tests/data/preview/comments7.py diff --git a/tests/data/comments8.py b/tests/data/preview/comments8.py similarity index 100% rename from tests/data/comments8.py rename to tests/data/preview/comments8.py diff --git a/tests/data/docstring_preview.py b/tests/data/preview/docstring_preview.py similarity index 100% rename from tests/data/docstring_preview.py rename to tests/data/preview/docstring_preview.py diff --git a/tests/data/long_strings.py b/tests/data/preview/long_strings.py similarity index 100% rename from tests/data/long_strings.py rename to tests/data/preview/long_strings.py diff --git a/tests/data/long_strings__edge_case.py b/tests/data/preview/long_strings__edge_case.py similarity index 100% rename from tests/data/long_strings__edge_case.py rename to tests/data/preview/long_strings__edge_case.py diff --git a/tests/data/long_strings__regression.py b/tests/data/preview/long_strings__regression.py similarity index 100% rename from tests/data/long_strings__regression.py rename to tests/data/preview/long_strings__regression.py diff --git a/tests/data/one_element_subscript.py b/tests/data/preview/one_element_subscript.py similarity index 100% rename from tests/data/one_element_subscript.py rename to tests/data/preview/one_element_subscript.py diff --git a/tests/data/percent_precedence.py b/tests/data/preview/percent_precedence.py similarity index 100% rename from tests/data/percent_precedence.py rename to tests/data/preview/percent_precedence.py diff --git a/tests/data/remove_await_parens.py b/tests/data/preview/remove_await_parens.py similarity index 100% rename from tests/data/remove_await_parens.py rename to tests/data/preview/remove_await_parens.py diff --git a/tests/data/remove_except_parens.py b/tests/data/preview/remove_except_parens.py similarity index 100% rename from tests/data/remove_except_parens.py rename to tests/data/preview/remove_except_parens.py diff --git a/tests/data/remove_for_brackets.py b/tests/data/preview/remove_for_brackets.py similarity index 100% rename from tests/data/remove_for_brackets.py rename to tests/data/preview/remove_for_brackets.py diff --git a/tests/data/return_annotation_brackets.py b/tests/data/preview/return_annotation_brackets.py similarity index 100% rename from tests/data/return_annotation_brackets.py rename to tests/data/preview/return_annotation_brackets.py diff --git a/tests/data/remove_with_brackets.py b/tests/data/preview_39/remove_with_brackets.py similarity index 100% rename from tests/data/remove_with_brackets.py rename to tests/data/preview_39/remove_with_brackets.py diff --git a/tests/data/parenthesized_context_managers.py b/tests/data/py_310/parenthesized_context_managers.py similarity index 100% rename from tests/data/parenthesized_context_managers.py rename to tests/data/py_310/parenthesized_context_managers.py diff --git a/tests/data/pattern_matching_complex.py b/tests/data/py_310/pattern_matching_complex.py similarity index 100% rename from tests/data/pattern_matching_complex.py rename to tests/data/py_310/pattern_matching_complex.py diff --git a/tests/data/pattern_matching_extras.py b/tests/data/py_310/pattern_matching_extras.py similarity index 100% rename from tests/data/pattern_matching_extras.py rename to tests/data/py_310/pattern_matching_extras.py diff --git a/tests/data/pattern_matching_generic.py b/tests/data/py_310/pattern_matching_generic.py similarity index 100% rename from tests/data/pattern_matching_generic.py rename to tests/data/py_310/pattern_matching_generic.py diff --git a/tests/data/pattern_matching_simple.py b/tests/data/py_310/pattern_matching_simple.py similarity index 100% rename from tests/data/pattern_matching_simple.py rename to tests/data/py_310/pattern_matching_simple.py diff --git a/tests/data/pattern_matching_style.py b/tests/data/py_310/pattern_matching_style.py similarity index 100% rename from tests/data/pattern_matching_style.py rename to tests/data/py_310/pattern_matching_style.py diff --git a/tests/data/pep_572_py310.py b/tests/data/py_310/pep_572_py310.py similarity index 100% rename from tests/data/pep_572_py310.py rename to tests/data/py_310/pep_572_py310.py diff --git a/tests/data/starred_for_target.py b/tests/data/py_310/starred_for_target.py similarity index 100% rename from tests/data/starred_for_target.py rename to tests/data/py_310/starred_for_target.py diff --git a/tests/data/pep_654.py b/tests/data/py_311/pep_654.py similarity index 100% rename from tests/data/pep_654.py rename to tests/data/py_311/pep_654.py diff --git a/tests/data/pep_654_style.py b/tests/data/py_311/pep_654_style.py similarity index 100% rename from tests/data/pep_654_style.py rename to tests/data/py_311/pep_654_style.py diff --git a/tests/data/numeric_literals.py b/tests/data/py_36/numeric_literals.py similarity index 100% rename from tests/data/numeric_literals.py rename to tests/data/py_36/numeric_literals.py diff --git a/tests/data/numeric_literals_skip_underscores.py b/tests/data/py_36/numeric_literals_skip_underscores.py similarity index 100% rename from tests/data/numeric_literals_skip_underscores.py rename to tests/data/py_36/numeric_literals_skip_underscores.py diff --git a/tests/data/python37.py b/tests/data/py_37/python37.py similarity index 100% rename from tests/data/python37.py rename to tests/data/py_37/python37.py diff --git a/tests/data/pep_570.py b/tests/data/py_38/pep_570.py similarity index 100% rename from tests/data/pep_570.py rename to tests/data/py_38/pep_570.py diff --git a/tests/data/pep_572.py b/tests/data/py_38/pep_572.py similarity index 100% rename from tests/data/pep_572.py rename to tests/data/py_38/pep_572.py diff --git a/tests/data/pep_572_remove_parens.py b/tests/data/py_38/pep_572_remove_parens.py similarity index 100% rename from tests/data/pep_572_remove_parens.py rename to tests/data/py_38/pep_572_remove_parens.py diff --git a/tests/data/python38.py b/tests/data/py_38/python38.py similarity index 100% rename from tests/data/python38.py rename to tests/data/py_38/python38.py diff --git a/tests/data/pep_572_py39.py b/tests/data/py_39/pep_572_py39.py similarity index 100% rename from tests/data/pep_572_py39.py rename to tests/data/py_39/pep_572_py39.py diff --git a/tests/data/python39.py b/tests/data/py_39/python39.py similarity index 100% rename from tests/data/python39.py rename to tests/data/py_39/python39.py diff --git a/tests/test_black.py b/tests/test_black.py index 281019a0bfa..a633e678dd7 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -60,6 +60,8 @@ ff, fs, read_data, + get_case_path, + read_data_from_file, ) THIS_FILE = Path(__file__) @@ -157,7 +159,7 @@ def test_experimental_string_processing_warns(self) -> None: ) def test_piping(self) -> None: - source, expected = read_data("src/black/__init__", data=False) + source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py") result = BlackRunner().invoke( black.main, [ @@ -179,8 +181,8 @@ def test_piping_diff(self) -> None: r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d " r"\+\d\d\d\d" ) - source, _ = read_data("simple_cases/expression.py") - expected, _ = read_data("simple_cases/expression.diff") + source, _ = read_data("simple_cases", "expression.py") + expected, _ = read_data("simple_cases", "expression.diff") args = [ "-", "--fast", @@ -197,7 +199,7 @@ def test_piping_diff(self) -> None: self.assertEqual(expected, actual) def test_piping_diff_with_color(self) -> None: - source, _ = read_data("simple_cases/expression.py") + source, _ = read_data("simple_cases", "expression.py") args = [ "-", "--fast", @@ -219,7 +221,7 @@ def test_piping_diff_with_color(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def _test_wip(self) -> None: - source, expected = read_data("wip") + source, expected = read_data("miscellaneous", "wip") sys.settrace(tracefunc) mode = replace( DEFAULT_MODE, @@ -233,7 +235,7 @@ def _test_wip(self) -> None: black.assert_stable(source, actual, black.FileMode()) def test_pep_572_version_detection(self) -> None: - source, _ = read_data("pep_572") + source, _ = read_data("py_38", "pep_572") root = black.lib2to3_parse(source) features = black.get_features_used(root) self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features) @@ -241,7 +243,7 @@ def test_pep_572_version_detection(self) -> None: self.assertIn(black.TargetVersion.PY38, versions) def test_expression_ff(self) -> None: - source, expected = read_data("simple_cases/expression.py") + source, expected = read_data("simple_cases", "expression.py") tmp_file = Path(black.dump_to_file(source)) try: self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES)) @@ -255,8 +257,8 @@ def test_expression_ff(self) -> None: black.assert_stable(source, actual, DEFAULT_MODE) def test_expression_diff(self) -> None: - source, _ = read_data("simple_cases/expression.py") - expected, _ = read_data("simple_cases/expression.diff") + source, _ = read_data("simple_cases", "expression.py") + expected, _ = read_data("simple_cases", "expression.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " @@ -281,8 +283,8 @@ def test_expression_diff(self) -> None: self.assertEqual(expected, actual, msg) def test_expression_diff_with_color(self) -> None: - source, _ = read_data("simple_cases/expression.py") - expected, _ = read_data("simple_cases/expression.diff") + source, _ = read_data("simple_cases", "expression.py") + expected, _ = read_data("simple_cases", "expression.diff") tmp_file = Path(black.dump_to_file(source)) try: result = BlackRunner().invoke( @@ -301,7 +303,7 @@ def test_expression_diff_with_color(self) -> None: self.assertIn("\033[0m", actual) def test_detect_pos_only_arguments(self) -> None: - source, _ = read_data("pep_570") + source, _ = read_data("py_38", "pep_570") root = black.lib2to3_parse(source) features = black.get_features_used(root) self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features) @@ -310,7 +312,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") + source, expected = read_data("miscellaneous", "string_quotes") mode = black.Mode(preview=True) assert_format(source, expected, mode) mode = replace(mode, string_normalization=False) @@ -320,8 +322,10 @@ def test_string_quotes(self) -> None: black.assert_stable(source, not_normalized, mode=mode) def test_skip_magic_trailing_comma(self) -> None: - source, _ = read_data("simple_cases/expression.py") - expected, _ = read_data("expression_skip_magic_trailing_comma.diff") + source, _ = read_data("simple_cases", "expression") + expected, _ = read_data( + "miscellaneous", "expression_skip_magic_trailing_comma.diff" + ) tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " @@ -348,8 +352,8 @@ def test_skip_magic_trailing_comma(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_async_as_identifier(self) -> None: - source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve() - source, expected = read_data("async_as_identifier") + source_path = get_case_path("miscellaneous", "async_as_identifier") + source, expected = read_data_from_file(source_path) actual = fs(source) self.assertFormatEqual(expected, actual) major, minor = sys.version_info[:2] @@ -363,8 +367,8 @@ def test_async_as_identifier(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_python37(self) -> None: - source_path = (THIS_DIR / "data" / "python37.py").resolve() - source, expected = read_data("python37") + source_path = get_case_path("py_37", "python37") + source, expected = read_data_from_file(source_path) actual = fs(source) self.assertFormatEqual(expected, actual) major, minor = sys.version_info[:2] @@ -712,7 +716,7 @@ def test_get_features_used_decorator(self) -> None: # since this makes some test cases of test_get_features_used() # fails if it fails, this is tested first so that a useful case # is identified - simples, relaxed = read_data("decorators") + simples, relaxed = read_data("miscellaneous", "decorators") # skip explanation comments at the top of the file for simple_test in simples.split("##")[1:]: node = black.lib2to3_parse(simple_test) @@ -755,7 +759,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) node = black.lib2to3_parse("123456\n") self.assertEqual(black.get_features_used(node), set()) - source, expected = read_data("simple_cases/function.py") + source, expected = read_data("simple_cases", "function") node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, @@ -765,7 +769,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), expected_features) node = black.lib2to3_parse(expected) self.assertEqual(black.get_features_used(node), expected_features) - source, expected = read_data("simple_cases/expression.py") + source, expected = read_data("simple_cases", "expression") node = black.lib2to3_parse(source) self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse(expected) @@ -851,8 +855,8 @@ def test_get_future_imports(self) -> None: @pytest.mark.incompatible_with_mypyc def test_debug_visitor(self) -> None: - source, _ = read_data("debug_visitor.py") - expected, _ = read_data("debug_visitor.out") + source, _ = read_data("miscellaneous", "debug_visitor") + expected, _ = read_data("miscellaneous", "debug_visitor.out") out_lines = [] err_lines = [] @@ -936,10 +940,10 @@ def test_works_in_mono_process_only_environment(self) -> None: def test_check_diff_use_together(self) -> None: with cache_dir(): # Files which will be reformatted. - src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() + src1 = get_case_path("miscellaneous", "string_quotes") self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) # Files which will not be reformatted. - src2 = (THIS_DIR / "data" / "simple_cases" / "composition.py").resolve() + src2 = get_case_path("simple_cases", "composition") self.invokeBlack([str(src2), "--diff", "--check"]) # Multi file command. self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) @@ -963,7 +967,7 @@ def test_broken_symlink(self) -> None: def test_single_file_force_pyi(self) -> None: pyi_mode = replace(DEFAULT_MODE, is_pyi=True) - contents, expected = read_data("force_pyi") + contents, expected = read_data("miscellaneous", "force_pyi") with cache_dir() as workspace: path = (workspace / "file.py").resolve() with open(path, "w") as fh: @@ -984,7 +988,7 @@ def test_single_file_force_pyi(self) -> None: def test_multi_file_force_pyi(self) -> None: reg_mode = DEFAULT_MODE pyi_mode = replace(DEFAULT_MODE, is_pyi=True) - contents, expected = read_data("force_pyi") + contents, expected = read_data("miscellaneous", "force_pyi") with cache_dir() as workspace: paths = [ (workspace / "file1.py").resolve(), @@ -1006,7 +1010,7 @@ def test_multi_file_force_pyi(self) -> None: self.assertNotIn(str(path), normal_cache) def test_pipe_force_pyi(self) -> None: - source, expected = read_data("force_pyi") + source, expected = read_data("miscellaneous", "force_pyi") result = CliRunner().invoke( black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8")) ) @@ -1017,7 +1021,7 @@ def test_pipe_force_pyi(self) -> None: def test_single_file_force_py36(self) -> None: reg_mode = DEFAULT_MODE py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - source, expected = read_data("force_py36") + source, expected = read_data("miscellaneous", "force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() with open(path, "w") as fh: @@ -1036,7 +1040,7 @@ def test_single_file_force_py36(self) -> None: def test_multi_file_force_py36(self) -> None: reg_mode = DEFAULT_MODE py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - source, expected = read_data("force_py36") + source, expected = read_data("miscellaneous", "force_py36") with cache_dir() as workspace: paths = [ (workspace / "file1.py").resolve(), @@ -1058,7 +1062,7 @@ def test_multi_file_force_py36(self) -> None: self.assertNotIn(str(path), normal_cache) def test_pipe_force_py36(self) -> None: - source, expected = read_data("force_py36") + source, expected = read_data("miscellaneous", "force_py36") result = CliRunner().invoke( black.main, ["-", "-q", "--target-version=py36"], @@ -1454,10 +1458,10 @@ def test_bpo_2142_workaround(self) -> None: # https://bugs.python.org/issue2142 - source, _ = read_data("missing_final_newline.py") + source, _ = read_data("miscellaneous", "missing_final_newline") # read_data adds a trailing newline source = source.rstrip() - expected, _ = read_data("missing_final_newline.diff") + expected, _ = read_data("miscellaneous", "missing_final_newline.diff") tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 6174c4538b9..75d756705be 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -95,7 +95,7 @@ async def check(header_value: str, expected_status: int = 400) -> None: @unittest_run_loop async def test_blackd_pyi(self) -> None: - source, expected = read_data("stub.pyi") + source, expected = read_data("miscellaneous", "stub.pyi") response = await self.client.post( "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} ) @@ -108,8 +108,8 @@ async def test_blackd_diff(self) -> None: r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" ) - source, _ = read_data("blackd_diff.py") - expected, _ = read_data("blackd_diff.diff") + source, _ = read_data("miscellaneous", "blackd_diff") + expected, _ = read_data("miscellaneous", "blackd_diff.diff") response = await self.client.post( "/", data=source, headers={blackd.DIFF_HEADER: "true"} diff --git a/tests/test_format.py b/tests/test_format.py index 003f5bbe188..005a5771c2b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -8,45 +8,12 @@ from tests.util import ( DEFAULT_MODE, PY36_VERSIONS, - THIS_DIR, assert_format, dump_to_stderr, read_data, all_data_cases, ) -PY310_CASES: List[str] = [ - "starred_for_target", - "pattern_matching_simple", - "pattern_matching_complex", - "pattern_matching_extras", - "pattern_matching_style", - "pattern_matching_generic", - "parenthesized_context_managers", -] - -PY311_CASES: List[str] = [ - "pep_654", - "pep_654_style", -] - -PREVIEW_CASES: List[str] = [ - # string processing - "cantfit", - "comments7", - "comments8", - "long_strings", - "long_strings__edge_case", - "long_strings__regression", - "percent_precedence", - "remove_except_parens", - "remove_for_brackets", - "one_element_subscript", - "remove_await_parens", - "return_annotation_brackets", - "docstring_preview", -] - SOURCES: List[str] = [ "src/black/__init__.py", "src/black/__main__.py", @@ -95,25 +62,33 @@ def patch_dump_to_file(request: Any) -> Iterator[None]: yield -def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None: - source, expected = read_data(filename, data=data) +def check_file( + subdir: str, filename: str, mode: black.Mode, *, data: bool = True +) -> None: + source, expected = read_data(subdir, filename, data=data) assert_format(source, expected, mode, fast=False) @pytest.mark.parametrize("filename", all_data_cases("simple_cases")) def test_simple_format(filename: str) -> None: - check_file(filename, DEFAULT_MODE) + check_file("simple_cases", filename, DEFAULT_MODE) -@pytest.mark.parametrize("filename", PREVIEW_CASES) +@pytest.mark.parametrize("filename", all_data_cases("preview")) def test_preview_format(filename: str) -> None: - check_file(filename, black.Mode(preview=True)) + check_file("preview", filename, black.Mode(preview=True)) + + +@pytest.mark.parametrize("filename", all_data_cases("preview_39")) +def test_preview_minimum_python_39_format(filename: str) -> None: + source, expected = read_data("preview_39", filename) + mode = black.Mode(preview=True) + assert_format(source, expected, mode, minimum_version=(3, 9)) @pytest.mark.parametrize("filename", SOURCES) def test_source_is_formatted(filename: str) -> None: - path = THIS_DIR.parent / filename - check_file(str(path), DEFAULT_MODE, data=False) + check_file("", filename, DEFAULT_MODE, data=False) # =============== # @@ -126,59 +101,50 @@ def test_empty() -> None: assert_format(source, expected) -def test_pep_572() -> None: - source, expected = read_data("pep_572") - assert_format(source, expected, minimum_version=(3, 8)) - - -def test_pep_572_remove_parens() -> None: - source, expected = read_data("pep_572_remove_parens") - assert_format(source, expected, minimum_version=(3, 8)) - - -def test_pep_572_do_not_remove_parens() -> None: - source, expected = read_data("pep_572_do_not_remove_parens") - # the AST safety checks will fail, but that's expected, just make sure no - # parentheses are touched - assert_format(source, expected, fast=True) +@pytest.mark.parametrize("filename", all_data_cases("py_36")) +def test_python_36(filename: str) -> None: + source, expected = read_data("py_36", filename) + mode = black.Mode(target_versions=PY36_VERSIONS) + assert_format(source, expected, mode, minimum_version=(3, 6)) -@pytest.mark.parametrize("major, minor", [(3, 9), (3, 10)]) -def test_pep_572_newer_syntax(major: int, minor: int) -> None: - source, expected = read_data(f"pep_572_py{major}{minor}") - assert_format(source, expected, minimum_version=(major, minor)) +@pytest.mark.parametrize("filename", all_data_cases("py_37")) +def test_python_37(filename: str) -> None: + source, expected = read_data("py_37", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY37}) + assert_format(source, expected, mode, minimum_version=(3, 7)) -def test_pep_570() -> None: - source, expected = read_data("pep_570") - assert_format(source, expected, minimum_version=(3, 8)) +@pytest.mark.parametrize("filename", all_data_cases("py_38")) +def test_python_38(filename: str) -> None: + source, expected = read_data("py_38", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY38}) + assert_format(source, expected, mode, minimum_version=(3, 8)) -def test_remove_with_brackets() -> None: - source, expected = read_data("remove_with_brackets") - assert_format( - source, - expected, - black.Mode(preview=True), - minimum_version=(3, 9), - ) +@pytest.mark.parametrize("filename", all_data_cases("py_39")) +def test_python_39(filename: str) -> None: + source, expected = read_data("py_39", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY39}) + assert_format(source, expected, mode, minimum_version=(3, 9)) -@pytest.mark.parametrize("filename", PY310_CASES) +@pytest.mark.parametrize("filename", all_data_cases("py_310")) def test_python_310(filename: str) -> None: - source, expected = read_data(filename) + source, expected = read_data("py_310", filename) mode = black.Mode(target_versions={black.TargetVersion.PY310}) assert_format(source, expected, mode, minimum_version=(3, 10)) -def test_python_310_without_target_version() -> None: - source, expected = read_data("pattern_matching_simple") +@pytest.mark.parametrize("filename", all_data_cases("py_310")) +def test_python_310_without_target_version(filename: str) -> None: + source, expected = read_data("py_310", filename) mode = black.Mode() assert_format(source, expected, mode, minimum_version=(3, 10)) def test_patma_invalid() -> None: - source, expected = read_data("pattern_matching_invalid") + source, expected = read_data("miscellaneous", "pattern_matching_invalid") mode = black.Mode(target_versions={black.TargetVersion.PY310}) with pytest.raises(black.parsing.InvalidInput) as exc_info: assert_format(source, expected, mode, minimum_version=(3, 10)) @@ -186,13 +152,19 @@ def test_patma_invalid() -> None: exc_info.match("Cannot parse: 10:11") -@pytest.mark.parametrize("filename", PY311_CASES) +@pytest.mark.parametrize("filename", all_data_cases("py_311")) def test_python_311(filename: str) -> None: - source, expected = read_data(filename) + source, expected = read_data("py_311", filename) mode = black.Mode(target_versions={black.TargetVersion.PY311}) assert_format(source, expected, mode, minimum_version=(3, 11)) +@pytest.mark.parametrize("filename", all_data_cases("fast")) +def test_fast_cases(filename: str) -> None: + source, expected = read_data("fast", filename) + assert_format(source, expected, fast=True) + + def test_python_2_hint() -> None: with pytest.raises(black.parsing.InvalidInput) as exc_info: assert_format("print 'daylily'", "print 'daylily'") @@ -201,47 +173,25 @@ def test_python_2_hint() -> None: def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" - source, expected = read_data("docstring_no_string_normalization") + source, expected = read_data("miscellaneous", "docstring_no_string_normalization") mode = replace(DEFAULT_MODE, string_normalization=False) assert_format(source, expected, mode) def test_long_strings_flag_disabled() -> None: """Tests for turning off the string processing logic.""" - source, expected = read_data("long_strings_flag_disabled") + source, expected = read_data("miscellaneous", "long_strings_flag_disabled") mode = replace(DEFAULT_MODE, experimental_string_processing=False) assert_format(source, expected, mode) -def test_numeric_literals() -> None: - source, expected = read_data("numeric_literals") - mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - assert_format(source, expected, mode) - - -def test_numeric_literals_ignoring_underscores() -> None: - source, expected = read_data("numeric_literals_skip_underscores") - mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) - assert_format(source, expected, mode) - - def test_stub() -> None: mode = replace(DEFAULT_MODE, is_pyi=True) - source, expected = read_data("stub.pyi") + source, expected = read_data("miscellaneous", "stub.pyi") assert_format(source, expected, mode) -def test_python38() -> None: - source, expected = read_data("python38") - assert_format(source, expected, minimum_version=(3, 8)) - - -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") + source, expected = read_data("miscellaneous", "power_op_newline") assert_format(source, expected, mode=black.Mode(line_length=0)) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 6f6b3090cd1..e1d7dd88dcb 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -17,7 +17,7 @@ import pytest from black import Mode from _pytest.monkeypatch import MonkeyPatch -from tests.util import DATA_DIR +from tests.util import DATA_DIR, read_jupyter_notebook, get_case_path with contextlib.suppress(ModuleNotFoundError): import IPython @@ -252,9 +252,7 @@ def test_empty_cell() -> None: def test_entire_notebook_empty_metadata() -> None: - with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd: - content_bytes = fd.read() - content = content_bytes.decode() + content = read_jupyter_notebook("jupyter", "notebook_empty_metadata") result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) expected = ( "{\n" @@ -289,9 +287,7 @@ def test_entire_notebook_empty_metadata() -> None: def test_entire_notebook_trailing_newline() -> None: - with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd: - content_bytes = fd.read() - content = content_bytes.decode() + content = read_jupyter_notebook("jupyter", "notebook_trailing_newline") result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) expected = ( "{\n" @@ -338,9 +334,7 @@ def test_entire_notebook_trailing_newline() -> None: def test_entire_notebook_no_trailing_newline() -> None: - with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd: - content_bytes = fd.read() - content = content_bytes.decode() + content = read_jupyter_notebook("jupyter", "notebook_no_trailing_newline") result = format_file_contents(content, fast=True, mode=JUPYTER_MODE) expected = ( "{\n" @@ -387,17 +381,14 @@ def test_entire_notebook_no_trailing_newline() -> None: def test_entire_notebook_without_changes() -> None: - with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd: - content_bytes = fd.read() - content = content_bytes.decode() + content = read_jupyter_notebook("jupyter", "notebook_without_changes") with pytest.raises(NothingChanged): format_file_contents(content, fast=True, mode=JUPYTER_MODE) def test_non_python_notebook() -> None: - with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd: - content_bytes = fd.read() - content = content_bytes.decode() + content = read_jupyter_notebook("jupyter", "non_python_notebook") + with pytest.raises(NothingChanged): format_file_contents(content, fast=True, mode=JUPYTER_MODE) @@ -408,7 +399,7 @@ def test_empty_string() -> None: def test_unparseable_notebook() -> None: - path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb" + path = get_case_path("jupyter", "notebook_which_cant_be_parsed.ipynb") msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\." with pytest.raises(ValueError, match=msg): format_file_in_place(path, fast=True, mode=JUPYTER_MODE) @@ -418,7 +409,7 @@ def test_ipynb_diff_with_change() -> None: result = runner.invoke( main, [ - str(DATA_DIR / "notebook_trailing_newline.ipynb"), + str(get_case_path("jupyter", "notebook_trailing_newline.ipynb")), "--diff", f"--config={EMPTY_CONFIG}", ], @@ -431,7 +422,7 @@ def test_ipynb_diff_with_no_change() -> None: result = runner.invoke( main, [ - str(DATA_DIR / "notebook_without_changes.ipynb"), + str(get_case_path("jupyter", "notebook_without_changes.ipynb")), "--diff", f"--config={EMPTY_CONFIG}", ], @@ -445,7 +436,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_single( ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() - nb = DATA_DIR / "notebook_trailing_newline.ipynb" + nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -471,7 +462,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( ) -> None: # Check that the cache isn't written to if Jupyter dependencies aren't installed. jupyter_dependencies_are_installed.cache_clear() - nb = DATA_DIR / "notebook_trailing_newline.ipynb" + nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -489,7 +480,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( def test_ipynb_flag(tmp_path: pathlib.Path) -> None: - nb = DATA_DIR / "notebook_trailing_newline.ipynb" + nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) @@ -507,7 +498,7 @@ def test_ipynb_flag(tmp_path: pathlib.Path) -> None: def test_ipynb_and_pyi_flags() -> None: - nb = DATA_DIR / "notebook_trailing_newline.ipynb" + nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") result = runner.invoke( main, [ diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index b03b8e13f14..a3c897760fb 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -1,8 +1,7 @@ import pytest -import os import pathlib -from tests.util import THIS_DIR +from tests.util import get_case_path from black import main, jupyter_dependencies_are_installed from click.testing import CliRunner @@ -13,7 +12,7 @@ def test_ipynb_diff_with_no_change_single() -> None: jupyter_dependencies_are_installed.cache_clear() - path = THIS_DIR / "data/notebook_trailing_newline.ipynb" + path = get_case_path("jupyter", "notebook_trailing_newline.ipynb") result = runner.invoke(main, [str(path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" @@ -25,7 +24,7 @@ def test_ipynb_diff_with_no_change_single() -> None: def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None: jupyter_dependencies_are_installed.cache_clear() runner = CliRunner() - nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" with open(nb) as src, open(tmp_nb, "w") as dst: dst.write(src.read()) diff --git a/tests/util.py b/tests/util.py index 1d76681dbea..d65c2e651ae 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,6 +11,9 @@ from black.mode import TargetVersion from black.output import diff, err, out +PYTHON_SUFFIX = ".py" +ALLOWED_SUFFIXES = (PYTHON_SUFFIX, ".pyi", ".out", ".diff", ".ipynb") + THIS_DIR = Path(__file__).parent DATA_DIR = THIS_DIR / "data" PROJECT_ROOT = THIS_DIR.parent @@ -90,21 +93,30 @@ def assertFormatEqual(self, expected: str, actual: str) -> None: _assert_format_equal(expected, actual) -def all_data_cases(dir_name: str, data: bool = True) -> List[str]: - base_dir = DATA_DIR if data else PROJECT_ROOT - cases_dir = base_dir / dir_name +def get_base_dir(data: bool) -> Path: + return DATA_DIR if data else PROJECT_ROOT + + +def all_data_cases(subdir_name: str, data: bool = True) -> List[str]: + cases_dir = get_base_dir(data) / subdir_name assert cases_dir.is_dir() - return [f"{dir_name}/{case_path.stem}" for case_path in cases_dir.iterdir()] + return [case_path.stem for case_path in cases_dir.iterdir()] -def read_data(name: str, data: bool = True) -> Tuple[str, str]: - """read_data('test_name') -> 'input', 'output'""" - if not name.endswith((".py", ".pyi", ".out", ".diff")): - name += ".py" - base_dir = DATA_DIR if data else PROJECT_ROOT - case_path = base_dir / name +def get_case_path( + subdir_name: str, name: str, data: bool = True, suffix: str = PYTHON_SUFFIX +) -> Path: + """Get case path from name""" + case_path = get_base_dir(data) / subdir_name / name + if not name.endswith(ALLOWED_SUFFIXES): + case_path = case_path.with_suffix(suffix) assert case_path.is_file(), f"{case_path} is not a file." - return read_data_from_file(case_path) + return case_path + + +def read_data(subdir_name: str, name: str, data: bool = True) -> Tuple[str, str]: + """read_data('test_name') -> 'input', 'output'""" + return read_data_from_file(get_case_path(subdir_name, name, data)) def read_data_from_file(file_name: Path) -> Tuple[str, str]: @@ -126,6 +138,18 @@ def read_data_from_file(file_name: Path) -> Tuple[str, str]: return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n" +def read_jupyter_notebook(subdir_name: str, name: str, data: bool = True) -> str: + return read_jupyter_notebook_from_file( + get_case_path(subdir_name, name, data, suffix=".ipynb") + ) + + +def read_jupyter_notebook_from_file(file_name: Path) -> str: + with open(file_name, mode="rb") as fd: + content_bytes = fd.read() + return content_bytes.decode() + + @contextmanager def change_directory(path: Path) -> Iterator[None]: """Context manager to temporarily chdir to a different directory.""" From fdb01f8622de9394fe9d8ee149985d365ca1419f Mon Sep 17 00:00:00 2001 From: laundmo Date: Sat, 21 May 2022 22:18:06 +0200 Subject: [PATCH 242/700] Document new Microsoft Black Formatter extension for VSCode (#3063) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/integrations/editors.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 1c7879b63a6..02baa29511b 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -301,9 +301,15 @@ close and reopen your File, _Black_ will be done with its job. ## Visual Studio Code -Use the -[Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) -([instructions](https://code.visualstudio.com/docs/python/editing#_formatting)). +- Use the + [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) + ([instructions](https://code.visualstudio.com/docs/python/editing#_formatting)). + +- Alternatively the pre-release + [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) + extension can be used which runs a [Language Server Protocol](https://langserver.org/) + server for Black. Formatting is much more responsive using this extension, **but the + minimum supported version of Black is 22.3.0**. ## SublimeText 3 From 9fe788d8704c1d4726b30e41e181c687c02cc1cf Mon Sep 17 00:00:00 2001 From: Yusuke Nishioka Date: Thu, 26 May 2022 23:44:26 +0900 Subject: [PATCH 243/700] Add more examples to exclude files in addition to the defaults (#3070) Co-authored-by: Jelle Zijlstra --- docs/usage_and_configuration/the_basics.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 4c793f459a2..c7e2d4a4dde 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -259,10 +259,14 @@ expressions by Black. Use `[ ]` to denote a significant space character. line-length = 88 target-version = ['py37'] include = '\.pyi?$' +# 'extend-exclude' excludes files or directories in addition to the defaults extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories # in the root of the project. -^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults) +( + ^/foo.py # exclude a file named foo.py in the root of the project + | *_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project +) ''' ``` From 1e557184b0a9f43bfbff862669966bc5328517e9 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 26 May 2022 19:45:22 +0300 Subject: [PATCH 244/700] Implement support for PEP 646 (#3071) --- CHANGES.md | 2 + src/black/__init__.py | 12 +++ src/black/mode.py | 2 + src/black/nodes.py | 13 ++- src/blib2to3/Grammar.txt | 9 +- src/blib2to3/pygram.py | 1 + tests/data/py_311/pep_646.py | 194 +++++++++++++++++++++++++++++++++++ tests/test_black.py | 6 ++ 8 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 tests/data/py_311/pep_646.py diff --git a/CHANGES.md b/CHANGES.md index 8f43431c842..6bc67f9db06 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -56,6 +56,8 @@ - [PEP 654](https://peps.python.org/pep-0654/#except) syntax (for example, `except *ExceptionGroup:`) is now supported (#3016) +- [PEP 646](https://peps.python.org/pep-0646) syntax (for example, + `Array[Batch, *Shape]` or `def fn(*args: *T) -> None`) is now supported (#3071) diff --git a/src/black/__init__.py b/src/black/__init__.py index 75321c3f35c..8872102a6ea 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1308,6 +1308,18 @@ def get_features_used( # noqa: C901 ): features.add(Feature.EXCEPT_STAR) + elif n.type in {syms.subscriptlist, syms.trailer} and any( + child.type == syms.star_expr for child in n.children + ): + features.add(Feature.VARIADIC_GENERICS) + + elif ( + n.type == syms.tname_star + and len(n.children) == 3 + and n.children[2].type == syms.star_expr + ): + features.add(Feature.VARIADIC_GENERICS) + return features diff --git a/src/black/mode.py b/src/black/mode.py index a418e0eb665..bf79f6a3148 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -49,6 +49,7 @@ class Feature(Enum): UNPACKING_ON_FLOW = 12 ANN_ASSIGN_EXTENDED_RHS = 13 EXCEPT_STAR = 14 + VARIADIC_GENERICS = 15 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -132,6 +133,7 @@ class Feature(Enum): Feature.ANN_ASSIGN_EXTENDED_RHS, Feature.PATTERN_MATCHING, Feature.EXCEPT_STAR, + Feature.VARIADIC_GENERICS, }, } diff --git a/src/black/nodes.py b/src/black/nodes.py index 37b96a498d6..918038f69ba 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -120,6 +120,7 @@ syms.term, syms.power, } +TYPED_NAMES: Final = {syms.tname, syms.tname_star} ASSIGNMENTS: Final = { "=", "+=", @@ -243,6 +244,14 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 # that, too. return prevp.prefix + elif ( + prevp.type == token.STAR + and parent_type(prevp) == syms.star_expr + and parent_type(prevp.parent) == syms.subscriptlist + ): + # No space between typevar tuples. + return NO + elif prevp.type in VARARGS_SPECIALS: if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS): return NO @@ -281,7 +290,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 return NO if t == token.EQUAL: - if prev.type != syms.tname: + if prev.type not in TYPED_NAMES: return NO elif prev.type == token.EQUAL: @@ -292,7 +301,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 elif prev.type != token.COMMA: return NO - elif p.type == syms.tname: + elif p.type in TYPED_NAMES: # type names if not prev: prevp = preceding_leaf(p) diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 1de54165513..ac7ad7643ff 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -24,7 +24,7 @@ parameters: '(' [typedargslist] ')' # arguments = argument (',' argument)* # argument = tfpdef ['=' test] # kwargs = '**' tname [','] -# args = '*' [tname] +# args = '*' [tname_star] # kwonly_kwargs = (',' argument)* [',' [kwargs]] # args_kwonly_kwargs = args kwonly_kwargs | kwargs # poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]] @@ -34,14 +34,15 @@ parameters: '(' [typedargslist] ')' # It needs to be fully expanded to allow our LL(1) parser to work on it. typedargslist: tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [ - ',' [((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])* + ',' [((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])* [',' ['**' tname [',']]] | '**' tname [',']) | tfpdef ['=' test] (',' tfpdef ['=' test])* [','])] - ] | ((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])* + ] | ((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])* [',' ['**' tname [',']]] | '**' tname [',']) | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) tname: NAME [':' test] +tname_star: NAME [':' (test|star_expr)] tfpdef: tname | '(' tfplist ')' tfplist: tfpdef (',' tfpdef)* [','] @@ -163,7 +164,7 @@ listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] ) lambdef: 'lambda' [varargslist] ':' test trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME -subscriptlist: subscript (',' subscript)* [','] +subscriptlist: (subscript|star_expr) (',' (subscript|star_expr))* [','] subscript: test [':=' test] | [test] ':' [test] [sliceop] sliceop: ':' [test] exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index a3df9be1265..99012cdd9cb 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -123,6 +123,7 @@ class _python_symbols(Symbols): tfpdef: int tfplist: int tname: int + tname_star: int trailer: int try_stmt: int typedargslist: int diff --git a/tests/data/py_311/pep_646.py b/tests/data/py_311/pep_646.py new file mode 100644 index 00000000000..e843ecf39d8 --- /dev/null +++ b/tests/data/py_311/pep_646.py @@ -0,0 +1,194 @@ +A[*b] +A[*b] = 1 +A +del A[*b] +A +A[*b, *b] +A[*b, *b] = 1 +A +del A[*b, *b] +A +A[b, *b] +A[b, *b] = 1 +A +del A[b, *b] +A +A[*b, b] +A[*b, b] = 1 +A +del A[*b, b] +A +A[b, b, *b] +A[b, b, *b] = 1 +A +del A[b, b, *b] +A +A[*b, b, b] +A[*b, b, b] = 1 +A +del A[*b, b, b] +A +A[b, *b, b] +A[b, *b, b] = 1 +A +del A[b, *b, b] +A +A[b, b, *b, b] +A[b, b, *b, b] = 1 +A +del A[b, b, *b, b] +A +A[b, *b, b, b] +A[b, *b, b, b] = 1 +A +del A[b, *b, b, b] +A +A[A[b, *b, b]] +A[A[b, *b, b]] = 1 +A +del A[A[b, *b, b]] +A +A[*A[b, *b, b]] +A[*A[b, *b, b]] = 1 +A +del A[*A[b, *b, b]] +A +A[b, ...] +A[b, ...] = 1 +A +del A[b, ...] +A +A[*A[b, ...]] +A[*A[b, ...]] = 1 +A +del A[*A[b, ...]] +A +l = [1, 2, 3] +A[*l] +A[*l] = 1 +A +del A[*l] +A +A[*l, 4] +A[*l, 4] = 1 +A +del A[*l, 4] +A +A[0, *l] +A[0, *l] = 1 +A +del A[0, *l] +A +A[1:2, *l] +A[1:2, *l] = 1 +A +del A[1:2, *l] +A +repr(A[1:2, *l]) == repr(A[1:2, 1, 2, 3]) +t = (1, 2, 3) +A[*t] +A[*t] = 1 +A +del A[*t] +A +A[*t, 4] +A[*t, 4] = 1 +A +del A[*t, 4] +A +A[0, *t] +A[0, *t] = 1 +A +del A[0, *t] +A +A[1:2, *t] +A[1:2, *t] = 1 +A +del A[1:2, *t] +A +repr(A[1:2, *t]) == repr(A[1:2, 1, 2, 3]) + + +def returns_list(): + return [1, 2, 3] + + +A[returns_list()] +A[returns_list()] = 1 +A +del A[returns_list()] +A +A[returns_list(), 4] +A[returns_list(), 4] = 1 +A +del A[returns_list(), 4] +A +A[*returns_list()] +A[*returns_list()] = 1 +A +del A[*returns_list()] +A +A[*returns_list(), 4] +A[*returns_list(), 4] = 1 +A +del A[*returns_list(), 4] +A +A[0, *returns_list()] +A[0, *returns_list()] = 1 +A +del A[0, *returns_list()] +A +A[*returns_list(), *returns_list()] +A[*returns_list(), *returns_list()] = 1 +A +del A[*returns_list(), *returns_list()] +A +A[1:2, *b] +A[*b, 1:2] +A[1:2, *b, 1:2] +A[*b, 1:2, *b] +A[1:, *b] +A[*b, 1:] +A[1:, *b, 1:] +A[*b, 1:, *b] +A[:1, *b] +A[*b, :1] +A[:1, *b, :1] +A[*b, :1, *b] +A[:, *b] +A[*b, :] +A[:, *b, :] +A[*b, :, *b] +A[a * b()] +A[a * b(), *c, *d(), e * f(g * h)] +A[a * b(), :] +A[a * b(), *c, *d(), e * f(g * h) :] +A[[b] * len(c), :] + + +def f1(*args: *b): + pass + + +f1.__annotations__ + + +def f2(*args: *b, arg1): + pass + + +f2.__annotations__ + + +def f3(*args: *b, arg1: int): + pass + + +f3.__annotations__ + + +def f4(*args: *b, arg1: int = 2): + pass + + +f4.__annotations__ diff --git a/tests/test_black.py b/tests/test_black.py index a633e678dd7..02a707e8996 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -804,6 +804,12 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse("try: pass\nexcept *Group: pass") self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) + node = black.lib2to3_parse("a[*b]") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + node = black.lib2to3_parse("a[x, *y(), z] = t") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + node = black.lib2to3_parse("def fn(*args: *T): pass") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From 436e12f2904e68acb57a059b1c44f1e0321e2d3f Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Wed, 1 Jun 2022 20:20:02 +0200 Subject: [PATCH 245/700] Add script to ease migration to black (#3038) * Add script to ease migration to black * Update CHANGES.md Co-authored-by: Cooper Lees --- CHANGES.md | 2 + scripts/migrate-black.py | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100755 scripts/migrate-black.py diff --git a/CHANGES.md b/CHANGES.md index 6bc67f9db06..a6b6594b57a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,8 @@ +- Add migrate-black.py script to ease migration to black formatted git project (#3038) + ### Output diff --git a/scripts/migrate-black.py b/scripts/migrate-black.py new file mode 100755 index 00000000000..5a6bc424824 --- /dev/null +++ b/scripts/migrate-black.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# check out every commit added by the current branch, blackify them, +# and generate diffs to reconstruct the original commits, but then +# blackified +import logging +import os +import sys +from subprocess import check_output, run, Popen, PIPE + + +def git(*args: str) -> str: + return check_output(["git"] + list(args)).decode("utf8").strip() + + +def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> int: + current_branch = git("branch", "--show-current") + + if not current_branch or base_branch == current_branch: + logger.error("You need to check out a feature brach to work on") + return 1 + + if not os.path.exists(".git"): + logger.error("Run me in the root of your repo") + return 1 + + merge_base = git("merge-base", "HEAD", base_branch) + if not merge_base: + logger.error( + "Could not find a common commit for current head and %s" % base_branch + ) + return 1 + + commits = git( + "log", "--reverse", "--pretty=format:%H", "%s~1..HEAD" % merge_base + ).split() + for commit in commits: + git("checkout", commit, "-b%s-black" % commit) + check_output(black_command, shell=True) + git("commit", "-aqm", "blackify") + + git("checkout", base_branch, "-b%s-black" % current_branch) + + for last_commit, commit in zip(commits, commits[1:]): + allow_empty = ( + b"--allow-empty" in run(["git", "apply", "-h"], stdout=PIPE).stdout + ) + quiet = b"--quiet" in run(["git", "apply", "-h"], stdout=PIPE).stdout + git_diff = Popen( + [ + "git", + "diff", + "--find-copies", + "%s-black..%s-black" % (last_commit, commit), + ], + stdout=PIPE, + ) + git_apply = Popen( + [ + "git", + "apply", + ] + + (["--quiet"] if quiet else []) + + [ + "-3", + "--intent-to-add", + ] + + (["--allow-empty"] if allow_empty else []) + + [ + "-", + ], + stdin=git_diff.stdout, + ) + if git_diff.stdout is not None: + git_diff.stdout.close() + git_apply.communicate() + git("commit", "--allow-empty", "-aqC", commit) + + for commit in commits: + git("branch", "-qD", "%s-black" % commit) + + return 0 + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("base_branch") + parser.add_argument("--black_command", default="black -q .") + parser.add_argument("--logfile", type=argparse.FileType("w"), default=sys.stdout) + args = parser.parse_args() + logger = logging.getLogger(__name__) + logger.addHandler(logging.StreamHandler(args.logfile)) + logger.setLevel(logging.INFO) + sys.exit(blackify(args.base_branch, args.black_command, logger)) From f51e53726b39a177355a7917c91c56f390dda7ef Mon Sep 17 00:00:00 2001 From: Vivek Vashist Date: Sat, 4 Jun 2022 08:29:26 +0930 Subject: [PATCH 246/700] Fix minor typo (#3096) --- scripts/migrate-black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/migrate-black.py b/scripts/migrate-black.py index 5a6bc424824..1183cb8a104 100755 --- a/scripts/migrate-black.py +++ b/scripts/migrate-black.py @@ -16,7 +16,7 @@ def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> in current_branch = git("branch", "--show-current") if not current_branch or base_branch == current_branch: - logger.error("You need to check out a feature brach to work on") + logger.error("You need to check out a feature branch to work on") return 1 if not os.path.exists(".git"): From 6d32ab02c535c3d41495f62288c9c3fc57e9a9bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jun 2022 20:29:32 -0400 Subject: [PATCH 247/700] Bump pre-commit/action from 2.0.3 to 3.0.0 (#3108) Bumps [pre-commit/action](https://github.com/pre-commit/action) from 2.0.3 to 3.0.0. - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](https://github.com/pre-commit/action/compare/v2.0.3...v3.0.0) --- updated-dependencies: - dependency-name: pre-commit/action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b630114882d..01cf31502b1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,4 +25,4 @@ jobs: python -m pip install -e '.[d]' - name: Lint - uses: pre-commit/action@v2.0.3 + uses: pre-commit/action@v3.0.0 From 4bb7bf2bdc95a8035ccf167023a7044e5f8e5ef6 Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Sat, 11 Jun 2022 09:55:01 +0300 Subject: [PATCH 248/700] Remove newline after code block open (#3035) Co-authored-by: Jelle Zijlstra --- AUTHORS.md | 1 + CHANGES.md | 1 + docs/the_black_code_style/future_style.md | 25 +++ src/black/lines.py | 13 ++ src/black/mode.py | 1 + .../remove_newline_after_code_block_open.py | 189 ++++++++++++++++++ .../preview_310/remove_newline_after match.py | 34 ++++ tests/test_black.py | 1 - tests/test_format.py | 7 + 9 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 tests/data/preview/remove_newline_after_code_block_open.py create mode 100644 tests/data/preview_310/remove_newline_after match.py diff --git a/AUTHORS.md b/AUTHORS.md index 8aa6263313e..faa2b05840f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -148,6 +148,7 @@ Multiple contributions by: - [Rishikesh Jha](mailto:rishijha424@gmail.com) - [Rupert Bedford](mailto:rupert@rupertb.com) - Russell Davis +- [Sagi Shadur](mailto:saroad2@gmail.com) - [Rémi Verschelde](mailto:rverschelde@gmail.com) - [Sami Salonen](mailto:sakki@iki.fi) - [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) diff --git a/CHANGES.md b/CHANGES.md index a6b6594b57a..7001271087a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Remove redundant parentheses around awaited objects (#2991) - Parentheses around return annotations are now managed (#2990) - Remove unnecessary parentheses from `with` statements (#2926) +- Remove trailing newlines after code block open (#3035) ### _Blackd_ diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 2ec2c0333a5..8d159e9b0a2 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -49,3 +49,28 @@ plain strings. User-made splits are respected when they do not exceed the line l limit. Line continuation backslashes are converted into parenthesized strings. Unnecessary parentheses are stripped. The stability and status of this feature is tracked in [this issue](https://github.com/psf/black/issues/2188). + +### Removing trailing newlines after code block open + +_Black_ will remove trailing newlines after code block openings. That means that the +following code: + +```python +def my_func(): + + print("The line above me will be deleted!") + + print("But the line above me won't!") +``` + +Will be changed to: + +```python +def my_func(): + print("The line above me will be deleted!") + + print("But the line above me won't!") +``` + +This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`, +`while`, `with`, `case` and `match`. diff --git a/src/black/lines.py b/src/black/lines.py index e455a507539..8b591c324a5 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -168,6 +168,13 @@ def is_triple_quoted_string(self) -> bool: and self.leaves[0].value.startswith(('"""', "'''")) ) + @property + def opens_block(self) -> bool: + """Does this line open a new level of indentation.""" + if len(self.leaves) == 0: + return False + return self.leaves[-1].type == token.COLON + def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool: """If so, needs to be split before emitting.""" for leaf in self.leaves: @@ -513,6 +520,12 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: ): return before, 1 + if ( + Preview.remove_block_trailing_newline in current_line.mode + and self.previous_line + and self.previous_line.opens_block + ): + return 0, 0 return before, 0 def _maybe_empty_lines_for_class_or_def( diff --git a/src/black/mode.py b/src/black/mode.py index bf79f6a3148..896c516df79 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -150,6 +150,7 @@ class Preview(Enum): one_element_subscript = auto() annotation_parens = auto() long_docstring_quotes_on_newline = auto() + remove_block_trailing_newline = auto() class Deprecated(UserWarning): diff --git a/tests/data/preview/remove_newline_after_code_block_open.py b/tests/data/preview/remove_newline_after_code_block_open.py new file mode 100644 index 00000000000..ef2e5c2f6f5 --- /dev/null +++ b/tests/data/preview/remove_newline_after_code_block_open.py @@ -0,0 +1,189 @@ +import random + + +def foo1(): + + print("The newline above me should be deleted!") + + +def foo2(): + + + + print("All the newlines above me should be deleted!") + + +def foo3(): + + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +def foo4(): + + # There is a comment here + + print("The newline above me should not be deleted!") + + +class Foo: + def bar(self): + + print("The newline above me should be deleted!") + + +for i in range(5): + + print(f"{i}) The line above me should be removed!") + + +for i in range(5): + + + + print(f"{i}) The lines above me should be removed!") + + +for i in range(5): + + for j in range(7): + + print(f"{i}) The lines above me should be removed!") + + +if random.randint(0, 3) == 0: + + print("The new line above me is about to be removed!") + + +if random.randint(0, 3) == 0: + + + + + print("The new lines above me is about to be removed!") + + +if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: + print("Two lines above me are about to be removed!") + + +while True: + + print("The newline above me should be deleted!") + + +while True: + + + + print("The newlines above me should be deleted!") + + +while True: + + while False: + + print("The newlines above me should be deleted!") + + +with open("/path/to/file.txt", mode="w") as file: + + file.write("The new line above me is about to be removed!") + + +with open("/path/to/file.txt", mode="w") as file: + + + + file.write("The new lines above me is about to be removed!") + + +with open("/path/to/file.txt", mode="r") as read_file: + + with open("/path/to/output_file.txt", mode="w") as write_file: + + write_file.writelines(read_file.readlines()) + +# output + +import random + + +def foo1(): + print("The newline above me should be deleted!") + + +def foo2(): + print("All the newlines above me should be deleted!") + + +def foo3(): + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +def foo4(): + # There is a comment here + + print("The newline above me should not be deleted!") + + +class Foo: + def bar(self): + print("The newline above me should be deleted!") + + +for i in range(5): + print(f"{i}) The line above me should be removed!") + + +for i in range(5): + print(f"{i}) The lines above me should be removed!") + + +for i in range(5): + for j in range(7): + print(f"{i}) The lines above me should be removed!") + + +if random.randint(0, 3) == 0: + print("The new line above me is about to be removed!") + + +if random.randint(0, 3) == 0: + print("The new lines above me is about to be removed!") + + +if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: + print("Two lines above me are about to be removed!") + + +while True: + print("The newline above me should be deleted!") + + +while True: + print("The newlines above me should be deleted!") + + +while True: + while False: + print("The newlines above me should be deleted!") + + +with open("/path/to/file.txt", mode="w") as file: + file.write("The new line above me is about to be removed!") + + +with open("/path/to/file.txt", mode="w") as file: + file.write("The new lines above me is about to be removed!") + + +with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) diff --git a/tests/data/preview_310/remove_newline_after match.py b/tests/data/preview_310/remove_newline_after match.py new file mode 100644 index 00000000000..f7bcfbf27a2 --- /dev/null +++ b/tests/data/preview_310/remove_newline_after match.py @@ -0,0 +1,34 @@ +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" + +# output +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 02a707e8996..8adcaed5ef8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1461,7 +1461,6 @@ def test_newline_comment_interaction(self) -> None: black.assert_stable(source, output, mode=DEFAULT_MODE) def test_bpo_2142_workaround(self) -> None: - # https://bugs.python.org/issue2142 source, _ = read_data("miscellaneous", "missing_final_newline") diff --git a/tests/test_format.py b/tests/test_format.py index 005a5771c2b..a8a922d17db 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -86,6 +86,13 @@ def test_preview_minimum_python_39_format(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 9)) +@pytest.mark.parametrize("filename", all_data_cases("preview_310")) +def test_preview_minimum_python_310_format(filename: str) -> None: + source, expected = read_data("preview_310", filename) + mode = black.Mode(preview=True) + assert_format(source, expected, mode, minimum_version=(3, 10)) + + @pytest.mark.parametrize("filename", SOURCES) def test_source_is_formatted(filename: str) -> None: check_file("", filename, DEFAULT_MODE, data=False) From 8c8675c62aef4fb662c686d19ebd35dc047258f0 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 11 Jun 2022 11:44:01 -0400 Subject: [PATCH 249/700] Update documentation dependencies (#3118) Furo, myst-parser, and Sphinx (had to pin docutils due to sphinx breakage) --- docs/conf.py | 2 +- docs/requirements.txt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e9fdebb5546..8da9c39ac41 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,7 +87,7 @@ def make_pypi_svg(version: str) -> None: # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/requirements.txt b/docs/requirements.txt index 72cf09fb6e9..a3c801ba613 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,9 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.17.2 -Sphinx==4.5.0 +myst-parser==0.18.0 +Sphinx==5.0.1 +# Older versions break Sphinx even though they're declared to be supported. +docutils==0.18.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.4.7 +furo==2022.6.4.1 From 162ecd1d2cf9471efefb5b61c17d28b73acb79a1 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 11 Jun 2022 17:04:09 +0100 Subject: [PATCH 250/700] Use is_number_token instead of assertion (#3069) --- src/black/__init__.py | 5 ++--- src/black/nodes.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 8872102a6ea..4200066e882 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -40,7 +40,7 @@ from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES from black.const import STDIN_PLACEHOLDER from black.nodes import STARS, syms, is_simple_decorator_expression -from black.nodes import is_string_token +from black.nodes import is_string_token, is_number_token from black.lines import Line, EmptyLineTracker from black.linegen import transform_line, LineGenerator, LN from black.comments import normalize_fmt_off @@ -1245,8 +1245,7 @@ def get_features_used( # noqa: C901 if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}: features.add(Feature.F_STRINGS) - elif n.type == token.NUMBER: - assert isinstance(n, Leaf) + elif is_number_token(n): if "_" in n.value: features.add(Feature.NUMERIC_UNDERSCORES) diff --git a/src/black/nodes.py b/src/black/nodes.py index 918038f69ba..12f24b96687 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -854,3 +854,7 @@ def is_rpar_token(nl: NL) -> TypeGuard[Leaf]: def is_string_token(nl: NL) -> TypeGuard[Leaf]: return nl.type == token.STRING + + +def is_number_token(nl: NL) -> TypeGuard[Leaf]: + return nl.type == token.NUMBER From 799adb53239e4a1e87253d40bc1cbd34f9103c52 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 13 Jun 2022 17:02:39 +0300 Subject: [PATCH 251/700] Bump actions/setup-python from 3 to 4 (#3121) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/diff_shades.yml | 8 ++++++-- .github/workflows/diff_shades_comment.yml | 4 +++- .github/workflows/doc.yml | 4 +++- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 4 +++- .github/workflows/pypi_upload.yml | 4 +++- .github/workflows/test.yml | 2 +- .github/workflows/upload_binary.yml | 2 +- .github/workflows/uvloop_test.yml | 4 +++- 9 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 749f87cdcdb..390089eca42 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -20,7 +20,9 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install diff-shades and support dependencies run: | @@ -54,7 +56,9 @@ jobs: # The baseline revision could be rather old so a full clone is ideal. fetch-depth: 0 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install diff-shades and support dependencies run: | diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 94302735d0a..a5d213875c7 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -13,7 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install support dependencies run: | diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index e2a0142cc65..97f5f01e1b5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -24,7 +24,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up latest Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install dependencies run: | diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index d796fd50564..4ee6c839b48 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01cf31502b1..fcfaa7885b1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install dependencies run: | diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index ef524a8ece6..cda215aa5d6 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -16,7 +16,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install latest pip, build, twine run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce481761aea..b99f9ddaa68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 6bb1d23306b..ed5ed961e67 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up latest Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "*" diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml index bbc39935f89..9f247826969 100644 --- a/.github/workflows/uvloop_test.yml +++ b/.github/workflows/uvloop_test.yml @@ -33,7 +33,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 + with: + python-version: "*" - name: Install latest pip run: | From 6c1bd08f16b636de38b92aeb2e0a1e8ebef0a0b1 Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Tue, 14 Jun 2022 19:08:36 +0300 Subject: [PATCH 252/700] Test run black on self (#3114) * Add run_self environment in tox * Add run_self task as part of the lint CI flow * Remove hard coded sources list * Remove black from pre-commit Co-authored-by: Cooper Lees --- .github/workflows/lint.yml | 5 ++++ .pre-commit-config.yaml | 8 ------- tests/test_format.py | 48 +------------------------------------- tox.ini | 9 ++++++- 4 files changed, 14 insertions(+), 56 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fcfaa7885b1..1dd5ab5d35e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,6 +25,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e '.[d]' + python -m pip install tox - name: Lint uses: pre-commit/action@v3.0.0 + + - name: Run On Self + run: | + tox -e run_self diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26b7fe8c791..a6dedc44968 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,14 +4,6 @@ exclude: ^(src/blib2to3/|profiling/|tests/data/) repos: - repo: local hooks: - - id: black - name: black - language: system - entry: black - minimum_pre_commit_version: 2.9.2 - require_serial: true - types_or: [python, pyi] - - id: check-pre-commit-rev-in-example name: Check pre-commit rev in example language: python diff --git a/tests/test_format.py b/tests/test_format.py index a8a922d17db..0e1059c61e4 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,5 +1,5 @@ from dataclasses import replace -from typing import Any, Iterator, List +from typing import Any, Iterator from unittest.mock import patch import pytest @@ -14,47 +14,6 @@ all_data_cases, ) -SOURCES: List[str] = [ - "src/black/__init__.py", - "src/black/__main__.py", - "src/black/brackets.py", - "src/black/cache.py", - "src/black/comments.py", - "src/black/concurrency.py", - "src/black/const.py", - "src/black/debug.py", - "src/black/files.py", - "src/black/linegen.py", - "src/black/lines.py", - "src/black/mode.py", - "src/black/nodes.py", - "src/black/numerics.py", - "src/black/output.py", - "src/black/parsing.py", - "src/black/report.py", - "src/black/rusty.py", - "src/black/strings.py", - "src/black/trans.py", - "src/blackd/__init__.py", - "src/blib2to3/pygram.py", - "src/blib2to3/pytree.py", - "src/blib2to3/pgen2/conv.py", - "src/blib2to3/pgen2/driver.py", - "src/blib2to3/pgen2/grammar.py", - "src/blib2to3/pgen2/literals.py", - "src/blib2to3/pgen2/parse.py", - "src/blib2to3/pgen2/pgen.py", - "src/blib2to3/pgen2/tokenize.py", - "src/blib2to3/pgen2/token.py", - "setup.py", - "tests/test_black.py", - "tests/test_blackd.py", - "tests/test_format.py", - "tests/optional.py", - "tests/util.py", - "tests/conftest.py", -] - @pytest.fixture(autouse=True) def patch_dump_to_file(request: Any) -> Iterator[None]: @@ -93,11 +52,6 @@ def test_preview_minimum_python_310_format(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 10)) -@pytest.mark.parametrize("filename", SOURCES) -def test_source_is_formatted(filename: str) -> None: - check_file("", filename, DEFAULT_MODE, data=False) - - # =============== # # Complex cases # ============= # diff --git a/tox.ini b/tox.ini index 258e6c5c203..7af9e48d6f0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310,py3},fuzz +envlist = {,ci-}py{36,37,38,39,310,py3},fuzz,run_self [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -61,3 +61,10 @@ commands = coverage erase coverage run fuzz.py coverage report + +[testenv:run_self] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +commands = + pip install -e .[d] + black --check {toxinidir}/src {toxinidir}/tests {toxinidir}/setup.py From e3c9b0430eae5de35fdbeed047f9b2f07f9b78de Mon Sep 17 00:00:00 2001 From: Nate Prewitt Date: Fri, 17 Jun 2022 13:37:33 -0600 Subject: [PATCH 253/700] Replace link to Requests documentation (#3125) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ffa7ef9522..624d4d755eb 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,8 @@ Twisted and CPython: > At least the name is good. -**Kenneth Reitz**, creator of [`requests`](http://python-requests.org/) and -[`pipenv`](https://readthedocs.org/projects/pipenv/): +**Kenneth Reitz**, creator of [`requests`](https://requests.readthedocs.io/en/latest/) +and [`pipenv`](https://readthedocs.org/projects/pipenv/): > This vastly improves the formatting of our code. Thanks a ton! From 6463fb874f6fd93d9a3b857e24987d5fa6ae0d57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jun 2022 10:22:24 -0400 Subject: [PATCH 254/700] Bump sphinx from 5.0.1 to 5.0.2 in /docs (#3128) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.1...v5.0.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... 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 a3c801ba613..528af3dbffd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.0 -Sphinx==5.0.1 +Sphinx==5.0.2 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 sphinxcontrib-programoutput==0.17 From fa6caa6ca8489103d22d23f8f4ae4d3569bb115e Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 23 Jun 2022 12:41:05 -0700 Subject: [PATCH 255/700] Only call get_future_imports when needed (#3135) --- 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 4200066e882..2d04cf81910 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1172,10 +1172,10 @@ def f( 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) if mode.target_versions: versions = mode.target_versions else: + future_imports = get_future_imports(src_node) versions = detect_target_versions(src_node, future_imports=future_imports) normalize_fmt_off(src_node, preview=mode.preview) From d848209d38cadaa060a023c223495e7874984ddc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jun 2022 09:54:49 -0400 Subject: [PATCH 256/700] Bump furo from 2022.6.4.1 to 2022.6.21 in /docs (#3138) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.6.4.1 to 2022.6.21. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.06.04.1...2022.06.21) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-patch ... 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 528af3dbffd..65387e05e7e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==5.0.2 docutils==0.18.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.6.4.1 +furo==2022.6.21 From eb5d175c9cd3c14a0731f8afd0cc5a18264353e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Mon, 27 Jun 2022 23:24:34 +0300 Subject: [PATCH 257/700] Update preview style docs to include recent changes (#3136) Covers GH-2926, GH-2990, GH-2991, and GH-3035. Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/the_black_code_style/future_style.md | 60 ++++++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 8d159e9b0a2..fab4bca120e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -50,27 +50,71 @@ 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). -### Removing trailing newlines after code block open +### Removing newlines in the beginning of code blocks -_Black_ will remove trailing newlines after code block openings. That means that the -following code: +_Black_ will remove newlines in the beginning of new code blocks, i.e. when the +indentation level is increased. For example: ```python def my_func(): print("The line above me will be deleted!") - - print("But the line above me won't!") ``` -Will be changed to: +will be changed to: ```python def my_func(): print("The line above me will be deleted!") - - print("But the line above me won't!") ``` This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`, `while`, `with`, `case` and `match`. + +### Improved parentheses management + +_Black_ will format parentheses around return annotations similarly to other sets of +parentheses. For example: + +```python +def foo() -> (int): + ... + +def foo() -> looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong: + ... +``` + +will be changed to: + +```python +def foo() -> int: + ... + + +def foo() -> ( + looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong +): + ... +``` + +And, extra parentheses in `await` expressions and `with` statements are removed. For +example: + +```python +with ((open("bla.txt")) as f, open("x")): + ... + +async def main(): + await (asyncio.sleep(1)) +``` + +will be changed to: + +```python +with open("bla.txt") as f, open("x"): + ... + + +async def main(): + await asyncio.sleep(1) +``` From f6c139c5215ce04fd3e73a900f1372942d58eca0 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:33:35 -0400 Subject: [PATCH 258/700] Prepare docs for release 22.6.0 (#3139) --- CHANGES.md | 55 +++++++++++++-------- docs/integrations/source_version_control.md | 9 ++-- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7001271087a..b33ae6d509e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,19 +10,10 @@ -- Fix unstable formatting involving `# fmt: skip` comments without internal spaces - (#2970) - ### Preview style -- Fixed bug where docstrings with triple quotes could exceed max line length (#3044) -- Remove redundant parentheses around awaited objects (#2991) -- Parentheses around return annotations are now managed (#2990) -- Remove unnecessary parentheses from `with` statements (#2926) -- Remove trailing newlines after code block open (#3035) - ### _Blackd_ @@ -40,18 +31,48 @@ -- Add migrate-black.py script to ease migration to black formatted git project (#3038) - ### Output -- Output python version and implementation as part of `--version` flag (#2997) - ### Packaging +### Parser + + + +### Performance + + + +## 22.6.0 + +### Style + +- Fix unstable formatting involving `#fmt: skip` and `# fmt:skip` comments (notice the + lack of spaces) (#2970) + +### Preview style + +- Docstring quotes are no longer moved if it would violate the line length limit (#3044) +- Parentheses around return annotations are now managed (#2990) +- Remove unnecessary parentheses around awaited objects (#2991) +- Remove unnecessary parentheses in `with` statements (#2926) +- Remove trailing newlines after code block open (#3035) + +### Integrations + +- Add `scripts/migrate-black.py` script to ease introduction of Black to a Git project + (#3038) + +### Output + +- Output Python version and implementation as part of `--version` flag (#2997) + +### Packaging + - Use `tomli` instead of `tomllib` on Python 3.11 builds where `tomllib` is not available (#2987) @@ -62,15 +83,9 @@ - [PEP 646](https://peps.python.org/pep-0646) syntax (for example, `Array[Batch, *Shape]` or `def fn(*args: *T) -> None`) is now supported (#3071) - - -### Performance - - - ### Vim Plugin -- Fixed strtobool function. It didn't parse true/on/false/off. (#3025) +- Fix `strtobool` function. It didn't parse true/on/false/off. (#3025) ## 22.3.0 diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index d7d3da47630..e897cf669fc 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: 22.3.0 + rev: 22.6.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -23,8 +23,11 @@ branches or other mutable refs since the hook [won't auto update as you may expect][pre-commit-mutable-rev]. If you want support for Jupyter Notebooks as well, then replace `id: black` with -`id: black-jupyter` (though note that it's only available from version `21.8b0` -onwards). +`id: black-jupyter`. + +```{note} +The `black-jupyter` hook is only available from version 21.8b0 and onwards. +``` [black-tags]: https://github.com/psf/black/tags [pre-commit-mutable-rev]: diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index c7e2d4a4dde..7f76c57d3e6 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 22.3.0 +black, version 22.6.0 ``` An option to require a specific version to be running is also provided. From 6debce63bc2429b1680f8838592f2e56e3df6b27 Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Tue, 28 Jun 2022 09:44:55 +0200 Subject: [PATCH 259/700] Fix typo in CHANGES.md (#3142) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b33ae6d509e..1d30045f48b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -114,7 +114,7 @@ ### Output -- In verbose, mode, log when _Black_ is using user-level config (#2861) +- In verbose mode, log when _Black_ is using user-level config (#2861) ### Packaging From b859a377c0bef3793fcceb0efd0086862f6a9365 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys <6032823+jack1142@users.noreply.github.com> Date: Sun, 3 Jul 2022 22:47:18 +0200 Subject: [PATCH 260/700] Use RTD's new build process and config (#3149) See the deprecation notice: https://docs.readthedocs.io/en/stable/config-file/v2.html#python-version --- .readthedocs.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 24eb3eaf6d9..fff2d6ed341 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,8 +3,12 @@ version: 2 formats: - htmlzip +build: + os: ubuntu-22.04 + tools: + python: "3.8" + python: - version: 3.8 install: - requirements: docs/requirements.txt From 7af77d1cf1fdeb54a45ddae422e1ebc3329129fa Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 6 Jul 2022 10:33:07 -0700 Subject: [PATCH 261/700] Stability policy: permit exceptional changes for unformatted code (#3155) --- CHANGES.md | 3 +++ docs/the_black_code_style/index.md | 15 +++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1d30045f48b..0bfa7ccd62c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,9 @@ +- Reword the stability policy to say that we may, in rare cases, make changes that + affect code that was not previously formatted by _Black_ (#3155) + ### Integrations diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md index d1508552fee..c7f29af6c73 100644 --- a/docs/the_black_code_style/index.md +++ b/docs/the_black_code_style/index.md @@ -24,13 +24,16 @@ below. Ongoing style considerations are tracked on GitHub with the The following policy applies for the _Black_ code style, in non pre-release versions of _Black_: -- The same code, formatted with the same options, will produce the same output for all - releases in a given calendar year. +- If code has been formatted with _Black_, it will remain unchanged when formatted with + the same options using any other release in the same calendar year. - This means projects can safely use `black ~= 22.0` without worrying about major - formatting changes disrupting their project in 2022. We may still fix bugs where - _Black_ crashes on some code, and make other improvements that do not affect - formatting. + This means projects can safely use `black ~= 22.0` without worrying about formatting + changes disrupting their project in 2022. We may still fix bugs where _Black_ crashes + on some code, and make other improvements that do not affect formatting. + + In rare cases, we may make changes affecting code that has not been previously + formatted with _Black_. For example, we have had bugs where we accidentally removed + some comments. Such bugs can be fixed without breaking the stability policy. - The first release in a new calendar year _may_ contain formatting changes, although these will be minimised as much as possible. This is to allow for improved formatting From 05b63c4bccbfb292a92d3ec962ce9b8fa4ebcfd5 Mon Sep 17 00:00:00 2001 From: Maciej Olko Date: Mon, 11 Jul 2022 18:27:51 +0200 Subject: [PATCH 262/700] Recommend using BlackConnect in IntelliJ IDEs (#3150) * Recommend using BlackConnect in IntelliJ IDEs * IntelliJ IDEs integration docs: improve formatting * Add changelog for recommending BlackConnect * IntelliJ IDEs integration docs: improve formatting * Apply suggestions from code review Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> * Fix indentation * Apply italic to Black name Consequently with other places in the document * Move CHANGELOG entry to Unreleased section * IntelliJ IDEs integration docs: bring back a point with formatting a file * IntelliJ IDEs integration docs: fix extra whitespace and linebreak Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + docs/integrations/editors.md | 66 ++++++++++-------------------------- 2 files changed, 19 insertions(+), 48 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0bfa7ccd62c..a0607edefa5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ - Reword the stability policy to say that we may, in rare cases, make changes that affect code that was not previously formatted by _Black_ (#3155) +- Recommend using BlackConnect in IntelliJ IDEs (#3150) ### Integrations diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 02baa29511b..07bf672f4fd 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -10,71 +10,41 @@ Options include the following: ## PyCharm/IntelliJ IDEA -1. Install `black`. +1. Install _Black_ with the `d` extra. ```console - $ pip install black + $ pip install 'black[d]' ``` -1. Locate your `black` installation folder. +1. Install + [BlackConnect IntelliJ IDEs plugin](https://plugins.jetbrains.com/plugin/14321-blackconnect). - On macOS / Linux / BSD: - - ```console - $ which black - /usr/local/bin/black # possible location - ``` - - On Windows: - - ```console - $ where black - %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location - ``` - - Note that if you are using a virtual environment detected by PyCharm, this is an - unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. - -1. Open External tools in PyCharm/IntelliJ IDEA +1. Open plugin configuration in PyCharm/IntelliJ IDEA On macOS: - `PyCharm -> Preferences -> Tools -> External Tools` + `PyCharm -> Preferences -> Tools -> BlackConnect` On Windows / Linux / BSD: - `File -> Settings -> Tools -> External Tools` - -1. Click the + icon to add a new external tool with the following values: + `File -> Settings -> Tools -> BlackConnect` - - Name: Black - - Description: Black is the uncompromising Python code formatter. - - Program: \ - - Arguments: `"$FilePath$"` +1. In `Local Instance (shared between projects)` section: -1. Format the currently opened file by selecting `Tools -> External Tools -> black`. + 1. Check `Start local blackd instance when plugin loads`. + 1. Press the `Detect` button near `Path` input. The plugin should detect the `blackd` + executable. - - Alternatively, you can set a keyboard shortcut by navigating to - `Preferences or Settings -> Keymap -> External Tools -> External Tools - Black`. +1. In `Trigger Settings` section check `Trigger on code reformat` to enable code + reformatting with _Black_. -1. Optionally, run _Black_ on every file save: +1. Format the currently opened file by selecting `Code -> Reformat Code` or using a + shortcut. - 1. Make sure you have the - [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin - installed. - 1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a - new watcher: - - Name: Black - - File type: Python - - Scope: Project Files - - Program: \ - - Arguments: `$FilePath$` - - Output paths to refresh: `$FilePath$` - - Working directory: `$ProjectFileDir$` +1. Optionally, to run _Black_ on every file save: - - In Advanced Options - - Uncheck "Auto-save edited files to trigger the watcher" - - Uncheck "Trigger the watcher on external changes" + - In `Trigger Settings` section of plugin configuration check + `Trigger when saving changed files`. ## Wing IDE From 18c17bea757dc88d0b6d2be6e99a4ebcc18e288c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 13 Jul 2022 20:02:51 -0400 Subject: [PATCH 263/700] Copy over comments when hugging power ops (#2874) Otherwise they'd be deleted which was a regression in 22.1.0 (oops! my bad!). Also type comments are now tracked in the AST safety check on all compatible platforms to error out if this happens again. Overall the line rewriting code has been rewritten to do "the right thing (tm)", I hope this fixes other potential bugs in the code (fwiw I got to drop the bugfix in blib2to3.pytree.Leaf.clone since now bracket metadata is properly copied over). Fixes #2873 --- CHANGES.md | 7 ++++++ src/black/parsing.py | 20 ++++++++++----- src/black/trans.py | 22 +++++++--------- src/blib2to3/pytree.py | 1 - tests/data/simple_cases/power_op_spacing.py | 28 +++++++++++++++++++++ 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a0607edefa5..249f7752bea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ +- Comments are no longer deleted when a line had spaces removed around power operators + (#2874) + ### Preview style @@ -47,6 +50,10 @@ +- Type comments are now included in the AST equivalence check consistently so accidental + deletion raises an error. Though type comments can't be tracked when running on PyPy + 3.7 due to standard library limitations. (#2874) + ### Performance diff --git a/src/black/parsing.py b/src/black/parsing.py index 12726567948..d1ad7d2c671 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -152,14 +152,22 @@ def parse_single_version( src: str, version: Tuple[int, int] ) -> Union[ast.AST, ast3.AST]: filename = "" - # typed_ast is needed because of feature version limitations in the builtin ast + # typed-ast is needed because of feature version limitations in the builtin ast 3.8> if sys.version_info >= (3, 8) and version >= (3,): - return ast.parse(src, filename, feature_version=version) - elif version >= (3,): - if _IS_PYPY: - return ast3.parse(src, filename) + return ast.parse(src, filename, feature_version=version, type_comments=True) + + if _IS_PYPY: + # PyPy 3.7 doesn't support type comment tracking which is not ideal, but there's + # not much we can do as typed-ast won't work either. + if sys.version_info >= (3, 8): + return ast3.parse(src, filename, type_comments=True) else: - return ast3.parse(src, filename, feature_version=version[1]) + return ast3.parse(src, filename) + else: + # Typed-ast is guaranteed to be used here and automatically tracks type + # comments separately. + return ast3.parse(src, filename, feature_version=version[1]) + raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!") diff --git a/src/black/trans.py b/src/black/trans.py index 01aa80eaaf8..28d9250adc1 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -121,7 +121,7 @@ def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool: return False - leaves: List[Leaf] = [] + new_line = line.clone() should_hug = False for idx, leaf in enumerate(line.leaves): new_leaf = leaf.clone() @@ -139,18 +139,14 @@ def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool: 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, - ) + # We have to be careful to make a new line properly: + # - bracket related metadata must be maintained (handled by Line.append) + # - comments need to copied over, updating the leaf IDs they're attached to + new_line.append(new_leaf, preformatted=True) + for comment_leaf in line.comments_after(leaf): + new_line.append(comment_leaf, preformatted=True) + + yield new_line class StringTransformer(ABC): diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index b203ce5b2ac..10b4690218e 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -451,7 +451,6 @@ 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/simple_cases/power_op_spacing.py b/tests/data/simple_cases/power_op_spacing.py index 87dde7f39dc..c95fa788fc3 100644 --- a/tests/data/simple_cases/power_op_spacing.py +++ b/tests/data/simple_cases/power_op_spacing.py @@ -49,6 +49,20 @@ def function_dont_replace_spaces(): q = [10.5**i for i in range(6)] +# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) +if hasattr(view, "sum_of_weights"): + return np.divide( # type: ignore[no-any-return] + view.variance, # type: ignore[union-attr] + view.sum_of_weights, # type: ignore[union-attr] + out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] + where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] + ) + +return np.divide( + where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore +) + + # output @@ -101,3 +115,17 @@ def function_dont_replace_spaces(): 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)] + + +# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) +if hasattr(view, "sum_of_weights"): + return np.divide( # type: ignore[no-any-return] + view.variance, # type: ignore[union-attr] + view.sum_of_weights, # type: ignore[union-attr] + out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] + where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] + ) + +return np.divide( + where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore +) From 4f0532d6f0d030799223453195069a282e111c8b Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 13 Jul 2022 22:26:05 -0400 Subject: [PATCH 264/700] Don't (ever) put a single-char closing docstring quote on a new line (#3166) Doing so is invalid. Note this only fixes the preview style since the logic putting closing docstring quotes on their own line if they violate the line length limit is quite new. Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 +++ src/black/linegen.py | 7 ++++--- tests/data/preview/docstring_preview.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 249f7752bea..09954f2b738 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,9 @@ +- Single-character closing docstring quotes are no longer moved to their own line as + this is invalid. This was a bug introduced in version 22.6.0. (#3166) + ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index ff54e05c4e6..20f3ac6fffb 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -330,13 +330,14 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # We could enforce triple quotes at this point. quote = quote_char * quote_len - if Preview.long_docstring_quotes_on_newline in self.mode: + # It's invalid to put closing single-character quotes on a new line. + if Preview.long_docstring_quotes_on_newline in self.mode and quote_len == 3: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. # If docstring is one line, then we need to add the length - # of the indent, prefix, and starting quotes. Ending quote are - # handled later + # of the indent, prefix, and starting quotes. Ending quotes are + # handled later. lines = docstring.splitlines() last_line_length = len(lines[-1]) if docstring else 0 diff --git a/tests/data/preview/docstring_preview.py b/tests/data/preview/docstring_preview.py index 2da4cd1acdb..292352c82f3 100644 --- a/tests/data/preview/docstring_preview.py +++ b/tests/data/preview/docstring_preview.py @@ -42,6 +42,14 @@ def multiline_docstring_at_line_limit_with_prefix(): second line----------------------------------------------------------------------""" +def single_quote_docstring_over_line_limit(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + +def single_quote_docstring_over_line_limit2(): + 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' + + # output @@ -87,3 +95,11 @@ def multiline_docstring_at_line_limit_with_prefix(): f"""first line---------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + +def single_quote_docstring_over_line_limit(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + +def single_quote_docstring_over_line_limit2(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." From 8900e3ac8a8f610d4c1b3fbe7bdf4f0358efc28d Mon Sep 17 00:00:00 2001 From: Nimrod <87605179+Panther-12@users.noreply.github.com> Date: Thu, 14 Jul 2022 22:22:29 +0300 Subject: [PATCH 265/700] Add warning to not run blackd publicly in docs (#3167) Co-authored-by: Jelle Zijlstra --- docs/usage_and_configuration/black_as_a_server.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index 7d07e94e6bb..fc9d1cab716 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -4,6 +4,11 @@ protocol. The main benefit of using it is to avoid the cost of starting up a new _Black_ process every time you want to blacken a file. +```{warning} +`blackd` should not be run as a publicly accessible server as there are no security +precautions in place to prevent abuse. **It is intended for local use only**. +``` + ## Usage `blackd` is not packaged alongside _Black_ by default because it has additional From 9aa33f467bafce081635ce88807d42b10b0a3105 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 14 Jul 2022 15:24:34 -0700 Subject: [PATCH 266/700] Move to explicitly creating a new loop (#3164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move to explicitly creating a new loop - >= 3.10 add a warning that `get_event_loop` will not automatically create a loop - Move to explicit API Test: - `python3.11 -m venv --upgrade-deps /tmp/tb` - `/tmp/tb/bin/pip install -e .` - Install deps and no blackd as aiohttp + yarl can't build still with 3.11 - https://github.com/aio-libs/aiohttp/issues/6600 - `export PYTHONWARNINGS=error` ``` cooper@l33t:~/repos/black$ /tmp/tb/bin/black . All done! ✨ 🍰 ✨ 44 files left unchanged. ``` Fixes #3110 * Add to CHANGES.md * Fix a cooper typo yet again * Set default asyncio loop to our explicitly created one + unset on exit * Update CHANGES.md Fix my silly typo. Co-authored-by: Thomas Grainger Co-authored-by: Cooper Ry Lees Co-authored-by: Thomas Grainger --- CHANGES.md | 3 +++ src/black/__init__.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 09954f2b738..7d2e0bc09d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,9 @@ +- Change from deprecated `asyncio.get_event_loop()` to create our event loop which + removes DeprecationWarning (#3164) + ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 2d04cf81910..d2df1cbee7c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -773,7 +773,6 @@ def reformat_many( from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor executor: Executor - loop = asyncio.get_event_loop() worker_count = workers if workers is not None else DEFAULT_WORKERS if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 @@ -788,6 +787,8 @@ def reformat_many( # any good due to the Global Interpreter Lock) executor = ThreadPoolExecutor(max_workers=1) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) try: loop.run_until_complete( schedule_formatting( @@ -801,7 +802,10 @@ def reformat_many( ) ) finally: - shutdown(loop) + try: + shutdown(loop) + finally: + asyncio.set_event_loop(None) if executor is not None: executor.shutdown() From ad5c315ddad26a3c41f22d3b73c493fb7d7b86b8 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 14 Jul 2022 19:47:33 -0400 Subject: [PATCH 267/700] Actually disable docstring prefix normalization with -S + fix instability (#3168) The former was a regression I introduced a long time ago. To avoid changing the stable style too much, the regression is only fixed if --preview is enabled Annoyingly enough, as we currently always enforce a second format pass if changes were made, there's no good way to prove the existence of the docstring quote normalization instability issue. For posterity, here's one failing example: --- source +++ first pass @@ -1,7 +1,7 @@ def some_function(self): - '''' + """ ' - ''' + """ pass --- first pass +++ second pass @@ -1,7 +1,7 @@ def some_function(self): - """ ' + """' """ pass Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 ++ src/black/linegen.py | 19 ++++++++++++++++++- src/black/mode.py | 7 ++++--- ...cstring_preview_no_string_normalization.py | 10 ++++++++++ tests/data/simple_cases/docstring.py | 14 ++++++++++++++ tests/test_format.py | 12 ++++++++++++ 6 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 tests/data/miscellaneous/docstring_preview_no_string_normalization.py diff --git a/CHANGES.md b/CHANGES.md index 7d2e0bc09d1..8543a8dbfe0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ - Single-character closing docstring quotes are no longer moved to their own line as this is invalid. This was a bug introduced in version 22.6.0. (#3166) +- `--skip-string-normalization` / `-S` now prevents docstring prefixes from being + normalized as expected (#3168) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 20f3ac6fffb..1f132b7888f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -293,7 +293,24 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. - docstring = normalize_string_prefix(leaf.value) + if Preview.normalize_docstring_quotes_and_prefixes_properly in self.mode: + # There was a bug where --skip-string-normalization wouldn't stop us + # from normalizing docstring prefixes. To maintain stability, we can + # only address this buggy behaviour while the preview style is enabled. + if self.mode.string_normalization: + docstring = normalize_string_prefix(leaf.value) + # visit_default() does handle string normalization for us, but + # since this method acts differently depending on quote style (ex. + # see padding logic below), there's a possibility for unstable + # formatting as visit_default() is called *after*. To avoid a + # situation where this function formats a docstring differently on + # the second pass, normalize it early. + docstring = normalize_string_quotes(docstring) + else: + docstring = leaf.value + else: + # ... otherwise, we'll keep the buggy behaviour >.< + docstring = normalize_string_prefix(leaf.value) prefix = get_string_prefix(docstring) docstring = docstring[len(prefix) :] # Remove the prefix quote_char = docstring[0] diff --git a/src/black/mode.py b/src/black/mode.py index 896c516df79..b7359fab213 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -145,12 +145,13 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" - string_processing = auto() - remove_redundant_parens = auto() - one_element_subscript = auto() annotation_parens = auto() long_docstring_quotes_on_newline = auto() + normalize_docstring_quotes_and_prefixes_properly = auto() + one_element_subscript = auto() remove_block_trailing_newline = auto() + remove_redundant_parens = auto() + string_processing = auto() class Deprecated(UserWarning): diff --git a/tests/data/miscellaneous/docstring_preview_no_string_normalization.py b/tests/data/miscellaneous/docstring_preview_no_string_normalization.py new file mode 100644 index 00000000000..0957231eb9c --- /dev/null +++ b/tests/data/miscellaneous/docstring_preview_no_string_normalization.py @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + F'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + uR'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/tests/data/simple_cases/docstring.py b/tests/data/simple_cases/docstring.py index 7153be468c1..f08bba575fe 100644 --- a/tests/data/simple_cases/docstring.py +++ b/tests/data/simple_cases/docstring.py @@ -209,6 +209,13 @@ def multiline_docstring_at_line_limit(): second line----------------------------------------------------------------------""" +def stable_quote_normalization_with_immediate_inner_single_quote(self): + '''' + + + ''' + + # output class MyClass: @@ -417,3 +424,10 @@ def multiline_docstring_at_line_limit(): """first line----------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + +def stable_quote_normalization_with_immediate_inner_single_quote(self): + """' + + + """ diff --git a/tests/test_format.py b/tests/test_format.py index 0e1059c61e4..86339f24b86 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -139,6 +139,18 @@ def test_docstring_no_string_normalization() -> None: assert_format(source, expected, mode) +def test_preview_docstring_no_string_normalization() -> None: + """ + Like test_docstring but with string normalization off *and* the preview style + enabled. + """ + source, expected = read_data( + "miscellaneous", "docstring_preview_no_string_normalization" + ) + mode = replace(DEFAULT_MODE, string_normalization=False, preview=True) + assert_format(source, expected, mode) + + def test_long_strings_flag_disabled() -> None: """Tests for turning off the string processing logic.""" source, expected = read_data("miscellaneous", "long_strings_flag_disabled") From b0eed7c6bd5f04f0ea6b6592d8a3ab63d8a01252 Mon Sep 17 00:00:00 2001 From: onescriptkid Date: Thu, 14 Jul 2022 16:51:18 -0700 Subject: [PATCH 268/700] Fix typo in config docs for --extend-exclude (#3170) The old regex in the example was invalid and caused an error on startup. --- docs/usage_and_configuration/the_basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 7f76c57d3e6..4c358742674 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -265,7 +265,7 @@ extend-exclude = ''' # in the root of the project. ( ^/foo.py # exclude a file named foo.py in the root of the project - | *_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project + | .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project ) ''' ``` From df5a87d93bd81c719a9c3f21da84133eaceef7b3 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 16 Jul 2022 13:18:55 +0100 Subject: [PATCH 269/700] configure strict pytest and filterwarnings=['error', ... (#3173) * configure strict pytest * ignore current warnings --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d9373740a5c..8b4b4ba7c0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] # Option below requires `tests/optional.py` +addopts = "--strict-config --strict-markers" optional-tests = [ "no_blackd: run when `d` extra NOT installed", "no_jupyter: run when `jupyter` extra NOT installed", @@ -38,3 +39,10 @@ optional-tests = [ markers = [ "incompatible_with_mypyc: run when testing mypyc compiled black" ] +xfail_strict = true +filterwarnings = [ + "error", + '''ignore:Decorator `@unittest_run_loop` is no longer needed in aiohttp 3\.8\+:DeprecationWarning''', + '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', + '''ignore:invalid escape sequence.*:DeprecationWarning''', +] From 33f0d9e79a098442135478b07b182a966f60375e Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 02:55:46 +0100 Subject: [PATCH 270/700] Add pypy-3.8 to test matrix (#3174) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b99f9ddaa68..7b4716c5493 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: From 1b6de7b0a33b568f71ff86e0e5fef6d4c479c2b7 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 19 Jul 2022 03:17:13 +0100 Subject: [PATCH 271/700] Improve warning filtering in tests (#3175) --- pyproject.toml | 5 ++++- tests/test_format.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b4b4ba7c0c..6df037c8a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,10 @@ markers = [ xfail_strict = true filterwarnings = [ "error", + # this is mitigated by a try/catch in https://github.com/psf/black/pull/2974/ + # this ignore can be removed when support for aiohttp 3.7 is dropped. '''ignore:Decorator `@unittest_run_loop` is no longer needed in aiohttp 3\.8\+:DeprecationWarning''', + # this is mitigated by https://github.com/python/cpython/issues/79071 in python 3.8+ + # this ignore can be removed when support for 3.7 is dropped. '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', - '''ignore:invalid escape sequence.*:DeprecationWarning''', ] diff --git a/tests/test_format.py b/tests/test_format.py index 86339f24b86..7a099fb9f33 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -28,6 +28,7 @@ def check_file( assert_format(source, expected, mode, fast=False) +@pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") @pytest.mark.parametrize("filename", all_data_cases("simple_cases")) def test_simple_format(filename: str) -> None: check_file("simple_cases", filename, DEFAULT_MODE) @@ -132,6 +133,7 @@ def test_python_2_hint() -> None: exc_info.match(black.parsing.PY2_HINT) +@pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" source, expected = read_data("miscellaneous", "docstring_no_string_normalization") From 6ea4eddf936e88c24a6757c0c858812d5ca1a9c6 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 19 Jul 2022 14:26:11 -0700 Subject: [PATCH 272/700] Fix the handling of `# fmt: skip` when it's at a colon line (#3148) When the Leaf node with `# fmt: skip` is a NEWLINE inside a `suite` Node, the nodes to ignore should be from the siblings of the parent `suite` Node. There is a also a special case for the ASYNC token, where it expands to the grandparent Node where the ASYNC token is. This fixes GH-2646, GH-3126, GH-2680, GH-2421, GH-2339, and GH-2138. --- CHANGES.md | 1 + src/black/comments.py | 74 +++++++++++++++++++++-------- src/black/linegen.py | 4 +- tests/data/simple_cases/fmtskip8.py | 62 ++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 tests/data/simple_cases/fmtskip8.py diff --git a/CHANGES.md b/CHANGES.md index 8543a8dbfe0..90c62de6b98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ +- Fix incorrect handling of `# fmt: skip` on colon `:` lines. (#3148) - Comments are no longer deleted when a line had spaces removed around power operators (#2874) diff --git a/src/black/comments.py b/src/black/comments.py index 23bf87fca7c..522c1a7b88c 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -9,7 +9,7 @@ else: from typing_extensions import Final -from blib2to3.pytree import Node, Leaf +from blib2to3.pytree import Node, Leaf, type_repr from blib2to3.pgen2 import token from black.nodes import first_leaf_column, preceding_leaf, container_of @@ -174,6 +174,11 @@ def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: first.prefix = prefix[comment.consumed :] if comment.value in FMT_SKIP: first.prefix = "" + standalone_comment_prefix = prefix + else: + standalone_comment_prefix = ( + prefix[:previous_consumed] + "\n" * comment.newlines + ) hidden_value = "".join(str(n) for n in ignored_nodes) if comment.value in FMT_OFF: hidden_value = comment.value + "\n" + hidden_value @@ -195,7 +200,7 @@ def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: Leaf( STANDALONE_COMMENT, hidden_value, - prefix=prefix[:previous_consumed] + "\n" * comment.newlines, + prefix=standalone_comment_prefix, ), ) return True @@ -211,26 +216,10 @@ def generate_ignored_nodes( If comment is skip, returns leaf only. Stops at the end of the block. """ - container: Optional[LN] = container_of(leaf) if comment.value in FMT_SKIP: - prev_sibling = leaf.prev_sibling - # Need to properly format the leaf prefix to compare it to comment.value, - # which is also formatted - comments = list_comments(leaf.prefix, is_endmarker=False, preview=preview) - if comments and comment.value == comments[0].value and prev_sibling is not None: - leaf.prefix = "" - siblings = [prev_sibling] - while ( - "\n" not in prev_sibling.prefix - and prev_sibling.prev_sibling is not None - ): - prev_sibling = prev_sibling.prev_sibling - siblings.insert(0, prev_sibling) - for sibling in siblings: - yield sibling - elif leaf.parent is not None: - yield leaf.parent + yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, preview=preview) return + container: Optional[LN] = container_of(leaf) while container is not None and container.type != token.ENDMARKER: if is_fmt_on(container, preview=preview): return @@ -246,6 +235,51 @@ def generate_ignored_nodes( container = container.next_sibling +def _generate_ignored_nodes_from_fmt_skip( + leaf: Leaf, comment: ProtoComment, *, preview: bool +) -> Iterator[LN]: + """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`.""" + prev_sibling = leaf.prev_sibling + parent = leaf.parent + # Need to properly format the leaf prefix to compare it to comment.value, + # which is also formatted + comments = list_comments(leaf.prefix, is_endmarker=False, preview=preview) + if not comments or comment.value != comments[0].value: + return + if prev_sibling is not None: + leaf.prefix = "" + siblings = [prev_sibling] + while "\n" not in prev_sibling.prefix and prev_sibling.prev_sibling is not None: + prev_sibling = prev_sibling.prev_sibling + siblings.insert(0, prev_sibling) + for sibling in siblings: + yield sibling + elif ( + parent is not None + and type_repr(parent.type) == "suite" + and leaf.type == token.NEWLINE + ): + # The `# fmt: skip` is on the colon line of the if/while/def/class/... + # statements. The ignored nodes should be previous siblings of the + # parent suite node. + leaf.prefix = "" + ignored_nodes: List[LN] = [] + parent_sibling = parent.prev_sibling + while parent_sibling is not None and type_repr(parent_sibling.type) != "suite": + ignored_nodes.insert(0, parent_sibling) + parent_sibling = parent_sibling.prev_sibling + # Special case for `async_stmt` where the ASYNC token is on the + # grandparent node. + grandparent = parent.parent + if ( + grandparent is not None + and grandparent.prev_sibling is not None + and grandparent.prev_sibling.type == token.ASYNC + ): + ignored_nodes.insert(0, grandparent.prev_sibling) + yield from iter(ignored_nodes) + + def is_fmt_on(container: LN, preview: bool) -> bool: """Determine whether formatting is switched on within a container. Determined by whether the last `# fmt:` comment is `on` or `off`. diff --git a/src/black/linegen.py b/src/black/linegen.py index 1f132b7888f..8e8d41e239a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -220,7 +220,9 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]: for child in children: yield from self.visit(child) - if child.type == token.ASYNC: + if child.type == token.ASYNC or child.type == STANDALONE_COMMENT: + # STANDALONE_COMMENT happens when `# fmt: skip` is applied on the async + # line. break internal_stmt = next(children) diff --git a/tests/data/simple_cases/fmtskip8.py b/tests/data/simple_cases/fmtskip8.py new file mode 100644 index 00000000000..38e9c2a9f47 --- /dev/null +++ b/tests/data/simple_cases/fmtskip8.py @@ -0,0 +1,62 @@ +# Make sure a leading comment is not removed. +def some_func( unformatted, args ): # fmt: skip + print("I am some_func") + return 0 + # Make sure this comment is not removed. + + +# Make sure a leading comment is not removed. +async def some_async_func( unformatted, args): # fmt: skip + print("I am some_async_func") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +class SomeClass( Unformatted, SuperClasses ): # fmt: skip + def some_method( self, unformatted, args ): # fmt: skip + print("I am some_method") + return 0 + + async def some_async_method( self, unformatted, args ): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +if unformatted_call( args ): # fmt: skip + print("First branch") + # Make sure this is not removed. +elif another_unformatted_call( args ): # fmt: skip + print("Second branch") +else : # fmt: skip + print("Last branch") + + +while some_condition( unformatted, args ): # fmt: skip + print("Do something") + + +for i in some_iter( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_for(): + async for i in some_async_iter( unformatted, args ): # fmt: skip + print("Do something") + + +try : # fmt: skip + some_call() +except UnformattedError as ex: # fmt: skip + handle_exception() +finally : # fmt: skip + finally_call() + + +with give_me_context( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_with(): + async with give_me_async_context( unformatted, args ): # fmt: skip + print("Do something") From 249c6536c4dff50773f30f222d1f81f0afe41f4c Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 19 Jul 2022 17:57:23 -0700 Subject: [PATCH 273/700] Fix an infinite loop when using `# fmt: on/off` ... (#3158) ... in the middle of an expression or code block by adding a missing return. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 ++ src/black/comments.py | 10 +++++++- tests/data/simple_cases/fmtonoff5.py | 36 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/data/simple_cases/fmtonoff5.py diff --git a/CHANGES.md b/CHANGES.md index 90c62de6b98..94c3bdda68e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ +- Fix an infinite loop when using `# fmt: on/off` in the middle of an expression or code + block (#3158) - Fix incorrect handling of `# fmt: skip` on colon `:` lines. (#3148) - Comments are no longer deleted when a line had spaces removed around power operators (#2874) diff --git a/src/black/comments.py b/src/black/comments.py index 522c1a7b88c..7069da16528 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -13,7 +13,7 @@ from blib2to3.pgen2 import token from black.nodes import first_leaf_column, preceding_leaf, container_of -from black.nodes import STANDALONE_COMMENT, WHITESPACE +from black.nodes import CLOSING_BRACKETS, STANDALONE_COMMENT, WHITESPACE # types LN = Union[Leaf, Node] @@ -227,6 +227,14 @@ def generate_ignored_nodes( # fix for fmt: on in children if contains_fmt_on_at_column(container, leaf.column, preview=preview): for child in container.children: + if isinstance(child, Leaf) and is_fmt_on(child, preview=preview): + if child.type in CLOSING_BRACKETS: + # This means `# fmt: on` is placed at a different bracket level + # than `# fmt: off`. This is an invalid use, but as a courtesy, + # we include this closing bracket in the ignored nodes. + # The alternative is to fail the formatting. + yield child + return if contains_fmt_on_at_column(child, leaf.column, preview=preview): return yield child diff --git a/tests/data/simple_cases/fmtonoff5.py b/tests/data/simple_cases/fmtonoff5.py new file mode 100644 index 00000000000..746aa41f4e4 --- /dev/null +++ b/tests/data/simple_cases/fmtonoff5.py @@ -0,0 +1,36 @@ +# Regression test for https://github.com/psf/black/issues/3129. +setup( + entry_points={ + # fmt: off + "console_scripts": [ + "foo-bar" + "=foo.bar.:main", + # fmt: on + ] # Includes an formatted indentation. + }, +) + + +# Regression test for https://github.com/psf/black/issues/2015. +run( + # fmt: off + [ + "ls", + "-la", + ] + # fmt: on + + path, + check=True, +) + + +# Regression test for https://github.com/psf/black/issues/3026. +def test_func(): + # yapf: disable + if unformatted( args ): + return True + # yapf: enable + elif b: + return True + + return False From b4dc40bf7aab4cd8914c3a2046c8ffc71bba2ff9 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 19 Jul 2022 18:33:00 -0700 Subject: [PATCH 274/700] Use underscores instead of a space in a test file's name (#3180) ... for *consistency* --- ...emove_newline_after match.py => remove_newline_after_match.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/data/preview_310/{remove_newline_after match.py => remove_newline_after_match.py} (100%) diff --git a/tests/data/preview_310/remove_newline_after match.py b/tests/data/preview_310/remove_newline_after_match.py similarity index 100% rename from tests/data/preview_310/remove_newline_after match.py rename to tests/data/preview_310/remove_newline_after_match.py From e9e756da7a68890fe36b79f711f93630758fd99d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Jul 2022 20:51:32 -0400 Subject: [PATCH 275/700] Bump sphinx from 5.0.2 to 5.1.0 in /docs (#3183) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.2 to 5.1.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.2...v5.1.0) --- updated-dependencies: - dependency-name: sphinx 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 65387e05e7e..e843a68566a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.0 -Sphinx==5.0.2 +Sphinx==5.1.0 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 sphinxcontrib-programoutput==0.17 From e0a780a5056f1039edcf12c7a44198be902afbbc Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:32:31 -0400 Subject: [PATCH 276/700] Add isort to linting toolchain Co-authored-by: Shivansh-007 --- .pre-commit-config.yaml | 5 +++++ pyproject.toml | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6dedc44968..3a9c0ceda85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,11 @@ repos: files: '(CHANGES\.md|the_basics\.md)$' additional_dependencies: *version_check_dependencies + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + - repo: https://github.com/pycqa/flake8 rev: 4.0.1 hooks: diff --git a/pyproject.toml b/pyproject.toml index 6df037c8a39..36765072056 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,13 +22,21 @@ extend-exclude = ''' # this off. preview = true -# Build system information below. +# Build system information and other project-specific configuration below. # NOTE: You don't need this in your own Black configuration. [build-system] requires = ["setuptools>=45.0", "setuptools_scm[toml]>=6.3.1", "wheel"] build-backend = "setuptools.build_meta" +[tool.isort] +atomic = true +profile = "black" +line_length = 88 +skip_gitignore = true +skip_glob = ["src/blib2to3", "tests/data", "profiling"] +known_first_party = ["black", "blib2to3", "blackd", "_black_version"] + [tool.pytest.ini_options] # Option below requires `tests/optional.py` addopts = "--strict-config --strict-markers" From 44d5da00b520a05cd56e58b3998660f64ea59ebd Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:33:08 -0400 Subject: [PATCH 277/700] Reformat codebase with isort --- action/main.py | 2 +- fuzz.py | 4 +- gallery/gallery.py | 10 +--- scripts/migrate-black.py | 2 +- setup.py | 5 +- src/black/__init__.py | 89 ++++++++++++++++++-------------- src/black/brackets.py | 20 ++++--- src/black/cache.py | 6 +-- src/black/comments.py | 15 ++++-- src/black/debug.py | 5 +- src/black/files.py | 8 +-- src/black/handle_ipynb_magics.py | 20 +++---- src/black/linegen.py | 73 ++++++++++++++++++-------- src/black/lines.py | 29 +++++++---- src/black/mode.py | 3 +- src/black/nodes.py | 20 ++----- src/black/output.py | 4 +- src/black/parsing.py | 8 ++- src/black/report.py | 2 +- src/black/rusty.py | 1 - src/black/trans.py | 38 +++++++++----- src/blackd/__init__.py | 5 +- src/blackd/middlewares.py | 7 +-- tests/optional.py | 6 +-- tests/test_black.py | 4 +- tests/test_blackd.py | 9 ++-- tests/test_format.py | 2 +- tests/test_ipynb.py | 15 +++--- tests/test_no_ipynb.py | 7 +-- tests/test_trans.py | 1 + 30 files changed, 235 insertions(+), 185 deletions(-) diff --git a/action/main.py b/action/main.py index d14b10f421d..cd920f5fe0d 100644 --- a/action/main.py +++ b/action/main.py @@ -2,7 +2,7 @@ import shlex import sys from pathlib import Path -from subprocess import run, PIPE, STDOUT +from subprocess import PIPE, STDOUT, run ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"]) ENV_PATH = ACTION_PATH / ".black-env" diff --git a/fuzz.py b/fuzz.py index f5f655ea279..83e02f45152 100644 --- a/fuzz.py +++ b/fuzz.py @@ -8,7 +8,8 @@ import re import hypothesmith -from hypothesis import HealthCheck, given, settings, strategies as st +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st import black from blib2to3.pgen2.tokenize import TokenError @@ -78,6 +79,7 @@ def test_idempotent_any_syntatically_valid_python( # (if you want only bounded fuzzing, just use `pytest fuzz.py`) try: import sys + import atheris except ImportError: pass diff --git a/gallery/gallery.py b/gallery/gallery.py index be4d81dc427..38e52e34795 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -10,15 +10,7 @@ from concurrent.futures import ThreadPoolExecutor from functools import lru_cache, partial from pathlib import Path -from typing import ( - Generator, - List, - NamedTuple, - Optional, - Tuple, - Union, - cast, -) +from typing import Generator, List, NamedTuple, Optional, Tuple, Union, cast from urllib.request import urlopen, urlretrieve PYPI_INSTANCE = "https://pypi.org/pypi" diff --git a/scripts/migrate-black.py b/scripts/migrate-black.py index 1183cb8a104..63cc5096a93 100755 --- a/scripts/migrate-black.py +++ b/scripts/migrate-black.py @@ -5,7 +5,7 @@ import logging import os import sys -from subprocess import check_output, run, Popen, PIPE +from subprocess import PIPE, Popen, check_output, run def git(*args: str) -> str: diff --git a/setup.py b/setup.py index 522a42a7ce2..3accdf433bc 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ # Copyright (C) 2020 Łukasz Langa -from setuptools import setup, find_packages -import sys import os +import sys + +from setuptools import find_packages, setup assert sys.version_info >= (3, 6, 2), "black requires Python 3.6.2+" from pathlib import Path # noqa E402 diff --git a/src/black/__init__.py b/src/black/__init__.py index d2df1cbee7c..2a5c750a583 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,20 +1,20 @@ import asyncio -from json.decoder import JSONDecodeError -import json -from contextlib import contextmanager -from datetime import datetime -from enum import Enum import io -from multiprocessing import Manager, freeze_support +import json import os -from pathlib import Path -from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import platform import re import signal import sys import tokenize import traceback +from contextlib import contextmanager +from dataclasses import replace +from datetime import datetime +from enum import Enum +from json.decoder import JSONDecodeError +from multiprocessing import Manager, freeze_support +from pathlib import Path from typing import ( TYPE_CHECKING, Any, @@ -34,48 +34,61 @@ import click from click.core import ParameterSource -from dataclasses import replace from mypy_extensions import mypyc_attr +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError -from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES -from black.const import STDIN_PLACEHOLDER -from black.nodes import STARS, syms, is_simple_decorator_expression -from black.nodes import is_string_token, is_number_token -from black.lines import Line, EmptyLineTracker -from black.linegen import transform_line, LineGenerator, LN +from _black_version import version as __version__ +from black.cache import Cache, filter_cached, get_cache_info, read_cache, write_cache from black.comments import normalize_fmt_off -from black.mode import FUTURE_FLAG_TO_FEATURE, Mode, TargetVersion -from black.mode import Feature, supports_feature, VERSION_TO_FEATURES -from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache -from black.concurrency import cancel, shutdown, maybe_install_uvloop -from black.output import dump_to_file, ipynb_diff, diff, color_diff, out, err -from black.report import Report, Changed, NothingChanged +from black.concurrency import cancel, maybe_install_uvloop, shutdown +from black.const import ( + DEFAULT_EXCLUDES, + DEFAULT_INCLUDES, + DEFAULT_LINE_LENGTH, + STDIN_PLACEHOLDER, +) from black.files import ( find_project_root, find_pyproject_toml, - parse_pyproject_toml, find_user_pyproject_toml, + gen_python_files, + get_gitignore, + normalize_path_maybe_ignore, + parse_pyproject_toml, + wrap_stream_for_windows, ) -from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore -from black.files import wrap_stream_for_windows -from black.parsing import InvalidInput # noqa F401 -from black.parsing import lib2to3_parse, parse_ast, stringify_ast from black.handle_ipynb_magics import ( - mask_cell, - unmask_cell, - remove_trailing_semicolon, - put_trailing_semicolon_back, - TRANSFORMED_MAGICS, PYTHON_CELL_MAGICS, + TRANSFORMED_MAGICS, jupyter_dependencies_are_installed, + mask_cell, + put_trailing_semicolon_back, + remove_trailing_semicolon, + unmask_cell, ) - - -# lib2to3 fork -from blib2to3.pytree import Node, Leaf +from black.linegen import LN, LineGenerator, transform_line +from black.lines import EmptyLineTracker, Line +from black.mode import ( + FUTURE_FLAG_TO_FEATURE, + VERSION_TO_FEATURES, + Feature, + Mode, + TargetVersion, + supports_feature, +) +from black.nodes import ( + STARS, + is_number_token, + is_simple_decorator_expression, + is_string_token, + syms, +) +from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out +from black.parsing import InvalidInput # noqa F401 +from black.parsing import lib2to3_parse, parse_ast, stringify_ast +from black.report import Changed, NothingChanged, Report from blib2to3.pgen2 import token - -from _black_version import version as __version__ +from blib2to3.pytree import Leaf, Node if TYPE_CHECKING: from concurrent.futures import Executor @@ -770,7 +783,7 @@ def reformat_many( workers: Optional[int], ) -> None: """Reformat multiple files using a ProcessPoolExecutor.""" - from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor + from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor executor: Executor worker_count = workers if workers is not None else DEFAULT_WORKERS diff --git a/src/black/brackets.py b/src/black/brackets.py index c5ed4bf5b9f..3566f5b6c37 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -1,7 +1,7 @@ """Builds on top of nodes.py to track brackets.""" -from dataclasses import dataclass, field import sys +from dataclasses import dataclass, field from typing import Dict, Iterable, List, Optional, Tuple, Union if sys.version_info < (3, 8): @@ -9,12 +9,20 @@ else: from typing import Final -from blib2to3.pytree import Leaf, Node +from black.nodes import ( + BRACKET, + CLOSING_BRACKETS, + COMPARATORS, + LOGIC_OPERATORS, + MATH_OPERATORS, + OPENING_BRACKETS, + UNPACKING_PARENTS, + VARARGS_PARENTS, + is_vararg, + syms, +) from blib2to3.pgen2 import token - -from black.nodes import syms, is_vararg, VARARGS_PARENTS, UNPACKING_PARENTS -from black.nodes import BRACKET, OPENING_BRACKETS, CLOSING_BRACKETS -from black.nodes import MATH_OPERATORS, COMPARATORS, LOGIC_OPERATORS +from blib2to3.pytree import Leaf, Node # types LN = Union[Leaf, Node] diff --git a/src/black/cache.py b/src/black/cache.py index 552c248d2ad..9455ff44772 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -2,16 +2,14 @@ import os import pickle -from pathlib import Path import tempfile +from pathlib import Path from typing import Dict, Iterable, Set, Tuple from platformdirs import user_cache_dir -from black.mode import Mode - from _black_version import version as __version__ - +from black.mode import Mode # types Timestamp = float diff --git a/src/black/comments.py b/src/black/comments.py index 7069da16528..dc58934f9d3 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,7 +1,7 @@ +import re import sys from dataclasses import dataclass from functools import lru_cache -import re from typing import Iterator, List, Optional, Union if sys.version_info >= (3, 8): @@ -9,11 +9,16 @@ else: from typing_extensions import Final -from blib2to3.pytree import Node, Leaf, type_repr +from black.nodes import ( + CLOSING_BRACKETS, + STANDALONE_COMMENT, + WHITESPACE, + container_of, + first_leaf_column, + preceding_leaf, +) from blib2to3.pgen2 import token - -from black.nodes import first_leaf_column, preceding_leaf, container_of -from black.nodes import CLOSING_BRACKETS, STANDALONE_COMMENT, WHITESPACE +from blib2to3.pytree import Leaf, Node, type_repr # types LN = Union[Leaf, Node] diff --git a/src/black/debug.py b/src/black/debug.py index 5143076ab35..150b44842dd 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -1,12 +1,11 @@ from dataclasses import dataclass from typing import Iterator, TypeVar, Union -from blib2to3.pytree import Node, Leaf, type_repr -from blib2to3.pgen2 import token - from black.nodes import Visitor from black.output import out from black.parsing import lib2to3_parse +from blib2to3.pgen2 import token +from blib2to3.pytree import Leaf, Node, type_repr LN = Union[Leaf, Node] T = TypeVar("T") diff --git a/src/black/files.py b/src/black/files.py index 0382397e8a2..17515d52b57 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -1,9 +1,10 @@ -from functools import lru_cache import io import os -from pathlib import Path import sys +from functools import lru_cache +from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, @@ -14,7 +15,6 @@ Sequence, Tuple, Union, - TYPE_CHECKING, ) from mypy_extensions import mypyc_attr @@ -30,9 +30,9 @@ else: import tomli as tomllib +from black.handle_ipynb_magics import jupyter_dependencies_are_installed from black.output import err from black.report import Report -from black.handle_ipynb_magics import jupyter_dependencies_are_installed if TYPE_CHECKING: import colorama # noqa: F401 diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index a0ed56baafc..693f1a68bd4 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -1,22 +1,20 @@ """Functions to process IPython magics with.""" -from functools import lru_cache -import dataclasses import ast -from typing import Dict, List, Tuple, Optional - +import collections +import dataclasses import secrets import sys -import collections +from functools import lru_cache +from typing import Dict, List, Optional, Tuple if sys.version_info >= (3, 10): from typing import TypeGuard else: from typing_extensions import TypeGuard -from black.report import NothingChanged from black.output import out - +from black.report import NothingChanged TRANSFORMED_MAGICS = frozenset( ( @@ -90,11 +88,7 @@ def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses ``tokenize_rt`` so that round-tripping works fine. """ - from tokenize_rt import ( - src_to_tokens, - tokens_to_src, - reversed_enumerate, - ) + from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src tokens = src_to_tokens(src) trailing_semicolon = False @@ -118,7 +112,7 @@ def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: """ if not has_trailing_semicolon: return src - from tokenize_rt import src_to_tokens, tokens_to_src, reversed_enumerate + from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src tokens = src_to_tokens(src) for idx, token in reversed_enumerate(tokens): diff --git a/src/black/linegen.py b/src/black/linegen.py index 8e8d41e239a..a2e41bf5912 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1,38 +1,67 @@ """ Generating lines of code. """ -from functools import partial, wraps import sys +from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast -from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT -from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS -from black.nodes import Visitor, syms, is_arith_like, ensure_visible +from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, max_delimiter_priority_in_atom +from black.comments import FMT_OFF, generate_comments, list_comments +from black.lines import ( + Line, + append_leaves, + can_be_split, + can_omit_invisible_parens, + is_line_short_enough, + line_to_string, +) +from black.mode import Feature, Mode, Preview from black.nodes import ( + ASSIGNMENTS, + CLOSING_BRACKETS, + OPENING_BRACKETS, + RARROW, + STANDALONE_COMMENT, + STATEMENT, + WHITESPACE, + Visitor, + ensure_visible, + is_arith_like, + is_atom_with_invisible_parens, is_docstring, is_empty_tuple, - is_one_tuple, + is_lpar_token, + is_multiline_string, + is_name_token, is_one_sequence_between, + is_one_tuple, + is_rpar_token, + is_stub_body, + is_stub_suite, + is_vararg, + is_walrus_assignment, + is_yield, + syms, + wrap_in_parentheses, ) -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 -from black.nodes import is_stub_suite, is_stub_body, is_atom_with_invisible_parens -from black.nodes import wrap_in_parentheses -from black.brackets import max_delimiter_priority_in_atom -from black.brackets import DOT_PRIORITY, COMMA_PRIORITY -from black.lines import Line, line_to_string, is_line_short_enough -from black.lines import can_omit_invisible_parens, can_be_split, append_leaves -from black.comments import generate_comments, list_comments, FMT_OFF 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, StringSplitter -from black.trans import StringParenWrapper, StringParenStripper, hug_power_op -from black.mode import Mode, Feature, Preview - -from blib2to3.pytree import Node, Leaf +from black.strings import ( + fix_docstring, + get_string_prefix, + normalize_string_prefix, + normalize_string_quotes, +) +from black.trans import ( + CannotTransform, + StringMerger, + StringParenStripper, + StringParenWrapper, + StringSplitter, + Transformer, + hug_power_op, +) from blib2to3.pgen2 import token - +from blib2to3.pytree import Leaf, Node # types LeafID = int diff --git a/src/black/lines.py b/src/black/lines.py index 8b591c324a5..1ebc7808901 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1,6 +1,6 @@ -from dataclasses import dataclass, field import itertools import sys +from dataclasses import dataclass, field from typing import ( Callable, Dict, @@ -13,16 +13,25 @@ cast, ) -from blib2to3.pytree import Node, Leaf -from blib2to3.pgen2 import token - -from black.brackets import BracketTracker, DOT_PRIORITY +from black.brackets import DOT_PRIORITY, BracketTracker from black.mode import Mode, Preview -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 -from black.nodes import is_one_sequence_between +from black.nodes import ( + BRACKETS, + CLOSING_BRACKETS, + OPENING_BRACKETS, + STANDALONE_COMMENT, + TEST_DESCENDANTS, + child_towards, + is_import, + is_multiline_string, + is_one_sequence_between, + is_type_comment, + replace_child, + syms, + whitespace, +) +from blib2to3.pgen2 import token +from blib2to3.pytree import Leaf, Node # types T = TypeVar("T") diff --git a/src/black/mode.py b/src/black/mode.py index b7359fab213..32b65d16ca5 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -4,11 +4,10 @@ chosen by the user. """ -from hashlib import sha256 import sys - from dataclasses import dataclass, field from enum import Enum, auto +from hashlib import sha256 from operator import attrgetter from typing import Dict, Set from warnings import warn diff --git a/src/black/nodes.py b/src/black/nodes.py index 12f24b96687..8f341ab35d6 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -3,16 +3,7 @@ """ import sys -from typing import ( - Generic, - Iterator, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union if sys.version_info >= (3, 8): from typing import Final @@ -25,14 +16,11 @@ from mypy_extensions import mypyc_attr -# lib2to3 fork -from blib2to3.pytree import Node, Leaf, type_repr, NL -from blib2to3 import pygram -from blib2to3.pgen2 import token - from black.cache import CACHE_DIR from black.strings import has_triple_quotes - +from blib2to3 import pygram +from blib2to3.pgen2 import token +from blib2to3.pytree import NL, Leaf, Node, type_repr pygram.initialize(CACHE_DIR) syms: Final = pygram.python_symbols diff --git a/src/black/output.py b/src/black/output.py index 9561d4b57d2..f4c17f28ea4 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -4,11 +4,11 @@ """ import json -from typing import Any, Optional -from mypy_extensions import mypyc_attr import tempfile +from typing import Any, Optional from click import echo, style +from mypy_extensions import mypyc_attr @mypyc_attr(patchable=True) diff --git a/src/black/parsing.py b/src/black/parsing.py index d1ad7d2c671..64c0b1e3018 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -11,16 +11,14 @@ else: from typing import Final -# lib2to3 fork -from blib2to3.pytree import Node, Leaf +from black.mode import Feature, TargetVersion, supports_feature +from black.nodes import syms from blib2to3 import pygram from blib2to3.pgen2 import driver from blib2to3.pgen2.grammar import Grammar from blib2to3.pgen2.parse import ParseError from blib2to3.pgen2.tokenize import TokenError - -from black.mode import TargetVersion, Feature, supports_feature -from black.nodes import syms +from blib2to3.pytree import Leaf, Node ast3: Any diff --git a/src/black/report.py b/src/black/report.py index 43b942c9e3c..a507671e4c0 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -7,7 +7,7 @@ from click import style -from black.output import out, err +from black.output import err, out class Changed(Enum): diff --git a/src/black/rusty.py b/src/black/rusty.py index 822e3d7858a..84a80b5a2c2 100644 --- a/src/black/rusty.py +++ b/src/black/rusty.py @@ -4,7 +4,6 @@ """ from typing import Generic, TypeVar, Union - T = TypeVar("T") E = TypeVar("E", bound=Exception) diff --git a/src/black/trans.py b/src/black/trans.py index 28d9250adc1..dc9c5520d5b 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1,10 +1,11 @@ """ String transformers that can split and merge strings. """ +import re +import sys from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass -import re from typing import ( Any, Callable, @@ -21,29 +22,38 @@ TypeVar, Union, ) -import sys if sys.version_info < (3, 8): - from typing_extensions import Literal, Final + from typing_extensions import Final, Literal else: from typing import Literal, Final from mypy_extensions import trait -from black.rusty import Result, Ok, Err - -from black.mode import Feature -from black.nodes import syms, replace_child, parent_type -from black.nodes import is_empty_par, is_empty_lpar, is_empty_rpar -from black.nodes import OPENING_BRACKETS, CLOSING_BRACKETS, STANDALONE_COMMENT -from black.lines import Line, append_leaves from black.brackets import BracketMatchError from black.comments import contains_pragma_comment -from black.strings import has_triple_quotes, get_string_prefix, assert_is_leaf_string -from black.strings import normalize_string_quotes - -from blib2to3.pytree import Leaf, Node +from black.lines import Line, append_leaves +from black.mode import Feature +from black.nodes import ( + CLOSING_BRACKETS, + OPENING_BRACKETS, + STANDALONE_COMMENT, + is_empty_lpar, + is_empty_par, + is_empty_rpar, + parent_type, + replace_child, + syms, +) +from black.rusty import Err, Ok, Result +from black.strings import ( + assert_is_leaf_string, + get_string_prefix, + has_triple_quotes, + normalize_string_quotes, +) from blib2to3.pgen2 import token +from blib2to3.pytree import Leaf, Node class CannotTransform(Exception): diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 0463f169e19..a6de79fbeaa 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -8,6 +8,7 @@ try: from aiohttp import web + from .middlewares import cors except ImportError as ie: raise ImportError( @@ -16,11 +17,11 @@ + "to obtain aiohttp_cors: `pip install black[d]`" ) from None -import black -from black.concurrency import maybe_install_uvloop import click +import black from _black_version import version as __version__ +from black.concurrency import maybe_install_uvloop # This is used internally by tests to shut down the server prematurely _stop_signal = asyncio.Event() diff --git a/src/blackd/middlewares.py b/src/blackd/middlewares.py index 97994ecc1df..7abde525bfd 100644 --- a/src/blackd/middlewares.py +++ b/src/blackd/middlewares.py @@ -1,7 +1,8 @@ -from typing import Iterable, Awaitable, Callable -from aiohttp.web_response import StreamResponse -from aiohttp.web_request import Request +from typing import Awaitable, Callable, Iterable + from aiohttp.web_middlewares import middleware +from aiohttp.web_request import Request +from aiohttp.web_response import StreamResponse Handler = Callable[[Request], Awaitable[StreamResponse]] Middleware = Callable[[Request, Handler], Awaitable[StreamResponse]] diff --git a/tests/optional.py b/tests/optional.py index a4e9441ef1c..853ecaa2a43 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -14,11 +14,11 @@ Adapted from https://pypi.org/project/pytest-optional-tests/, (c) 2019 Reece Hart """ -from functools import lru_cache import itertools import logging import re -from typing import FrozenSet, List, Set, TYPE_CHECKING +from functools import lru_cache +from typing import TYPE_CHECKING, FrozenSet, List, Set import pytest @@ -32,8 +32,8 @@ if TYPE_CHECKING: - from _pytest.config.argparsing import Parser from _pytest.config import Config + from _pytest.config.argparsing import Parser from _pytest.mark.structures import MarkDecorator from _pytest.nodes import Node diff --git a/tests/test_black.py b/tests/test_black.py index 8adcaed5ef8..bb7784d5478 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -6,6 +6,7 @@ import logging import multiprocessing import os +import re import sys import types import unittest @@ -31,7 +32,6 @@ import click import pytest -import re from click import unstyle from click.testing import CliRunner from pathspec import PathSpec @@ -59,8 +59,8 @@ dump_to_stderr, ff, fs, - read_data, get_case_path, + read_data, read_data_from_file, ) diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 75d756705be..18b2c98ac1f 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -2,15 +2,16 @@ from typing import Any from unittest.mock import patch -from click.testing import CliRunner import pytest +from click.testing import CliRunner -from tests.util import read_data, DETERMINISTIC_HEADER +from tests.util import DETERMINISTIC_HEADER, read_data try: - import blackd - from aiohttp.test_utils import AioHTTPTestCase from aiohttp import web + from aiohttp.test_utils import AioHTTPTestCase + + import blackd except ImportError as e: raise RuntimeError("Please install Black with the 'd' extra") from e diff --git a/tests/test_format.py b/tests/test_format.py index 7a099fb9f33..3645934721f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -8,10 +8,10 @@ from tests.util import ( DEFAULT_MODE, PY36_VERSIONS, + all_data_cases, assert_format, dump_to_stderr, read_data, - all_data_cases, ) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index e1d7dd88dcb..7aa2e91dd00 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -1,23 +1,24 @@ import contextlib -from dataclasses import replace import pathlib import re from contextlib import ExitStack as does_not_raise +from dataclasses import replace from typing import ContextManager +import pytest +from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner -from black.handle_ipynb_magics import jupyter_dependencies_are_installed + from black import ( - main, + Mode, NothingChanged, format_cell, format_file_contents, format_file_in_place, + main, ) -import pytest -from black import Mode -from _pytest.monkeypatch import MonkeyPatch -from tests.util import DATA_DIR, read_jupyter_notebook, get_case_path +from black.handle_ipynb_magics import jupyter_dependencies_are_installed +from tests.util import DATA_DIR, get_case_path, read_jupyter_notebook with contextlib.suppress(ModuleNotFoundError): import IPython diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index a3c897760fb..3e0b1593bf0 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -1,10 +1,11 @@ -import pytest import pathlib -from tests.util import get_case_path -from black import main, jupyter_dependencies_are_installed +import pytest from click.testing import CliRunner +from black import jupyter_dependencies_are_installed, main +from tests.util import get_case_path + pytestmark = pytest.mark.no_jupyter runner = CliRunner() diff --git a/tests/test_trans.py b/tests/test_trans.py index a1666a9c166..dce8a939677 100644 --- a/tests/test_trans.py +++ b/tests/test_trans.py @@ -1,4 +1,5 @@ from typing import List, Tuple + from black.trans import iter_fexpr_spans From 411ed778d53244a9d0b9c1913266fd03aee89123 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 27 Jul 2022 22:04:14 -0400 Subject: [PATCH 278/700] Bump pre-commit hooks (#3191) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a9c0ceda85..87bb6e62987 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.971 hooks: - id: mypy exclude: ^docs/conf.py @@ -51,13 +51,13 @@ repos: - platformdirs >= 2.1.0 - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.5.1 + rev: v2.7.1 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.3.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace From ef8deb6d4a729192d7b7818d91530d462e769b7d Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 28 Jul 2022 16:55:36 -0400 Subject: [PATCH 279/700] Consolidate test CI and add concurrency limits (#3189) --- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 4 ++ .github/workflows/lint.yml | 6 +-- .github/workflows/test.yml | 64 +++++++++++++++++++++---------- .github/workflows/uvloop_test.yml | 50 ------------------------ 5 files changed, 52 insertions(+), 74 deletions(-) delete mode 100644 .github/workflows/uvloop_test.yml diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 97f5f01e1b5..fc94dea62d9 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,4 +1,4 @@ -name: Documentation Build +name: Documentation on: [push, pull_request] diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4ee6c839b48..a2810e25f77 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -2,6 +2,10 @@ name: Fuzz on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1dd5ab5d35e..90c48013080 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python + - name: Set up latest Python uses: actions/setup-python@v4 with: python-version: "*" @@ -27,9 +27,9 @@ jobs: python -m pip install -e '.[d]' python -m pip install tox - - name: Lint + - name: Run pre-commit hooks uses: pre-commit/action@v3.0.0 - - name: Run On Self + - name: Format ourselves run: | tox -e run_self diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b4716c5493..7cc55d1bf76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,15 @@ on: - "docs/**" - "*.md" +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: - build: + main: # 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. @@ -35,29 +42,23 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install tox run: | python -m pip install --upgrade pip python -m pip install --upgrade tox - name: Unit tests if: "!startsWith(matrix.python-version, 'pypy')" - run: | - tox -e ci-py -- -v --color=yes + run: tox -e ci-py -- -v --color=yes - - name: Unit tests pypy + - name: Unit tests (pypy) if: "startsWith(matrix.python-version, 'pypy')" - run: | - tox -e ci-pypy3 -- -v --color=yes + run: tox -e ci-pypy3 -- -v --color=yes - - name: Publish coverage to Coveralls - # If pushed / is a pull request against main repo AND + - name: Upload coverage to Coveralls + # Upload coverage if we are on the main repository and # we're running on Linux (this action only supports Linux) - if: - ((github.event_name == 'push' && github.repository == 'psf/black') || - github.event.pull_request.base.repo.full_name == 'psf/black') && matrix.os == - 'ubuntu-latest' - + if: github.repository == 'psf/black' && matrix.os == 'ubuntu-latest' uses: AndreMiras/coveralls-python-action@v20201129 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -66,17 +67,40 @@ jobs: debug: true coveralls-finish: - needs: build - # If pushed / is a pull request against main repo - if: - (github.event_name == 'push' && github.repository == 'psf/black') || - github.event.pull_request.base.repo.full_name == 'psf/black' + needs: main + if: github.repository == 'psf/black' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Coveralls finished + - name: Send finished signal to Coveralls uses: AndreMiras/coveralls-python-action@v20201129 with: parallel-finished: true debug: true + + uvloop: + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != + github.repository + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up latest Python + uses: actions/setup-python@v4 + with: + python-version: "*" + + - name: Install black with uvloop + run: | + python -m pip install pip --upgrade --disable-pip-version-check + python -m pip install -e ".[uvloop]" + + - name: Format ourselves + run: python -m black --check src/ diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml deleted file mode 100644 index 9f247826969..00000000000 --- a/.github/workflows/uvloop_test.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: test uvloop - -on: - push: - paths-ignore: - - "docs/**" - - "*.md" - - pull_request: - paths-ignore: - - "docs/**" - - "*.md" - -permissions: - contents: read - -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: - os: [ubuntu-latest, macOS-latest] - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: "*" - - - name: Install latest pip - run: | - python -m pip install --upgrade pip - - - name: Test uvloop Extra Install - run: | - python -m pip install -e ".[uvloop]" - - - name: Format ourselves - run: | - python -m black --check src/ From 4f1772e2aed8356e57b923eacf45f813ec3324a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Fri, 29 Jul 2022 01:49:00 +0200 Subject: [PATCH 280/700] Vim plugin: prefix messages with "Black: " (#3194) As mentioned in GH-3185, when using Black as a Vim plugin, especially automatically on save, the plugin's messages can be confusing, as nothing indicates that they come from Black. --- CHANGES.md | 2 ++ autoload/black.vim | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 94c3bdda68e..e027b2cae71 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,6 +46,8 @@ +- Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194) + ### Output diff --git a/autoload/black.vim b/autoload/black.vim index 6c381b431a3..ed657be7bd3 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -158,9 +158,9 @@ def Black(**kwargs): ) except black.NothingChanged: if not quiet: - print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)') + print(f'Black: already well formatted, good job. (took {time.time() - start:.4f}s)') except Exception as exc: - print(exc) + print(f'Black: {exc}') else: current_buffer = vim.current.window.buffer cursors = [] @@ -177,7 +177,7 @@ def Black(**kwargs): except vim.error: window.cursor = (len(window.buffer), 0) if not quiet: - print(f'Reformatted in {time.time() - start:.4f}s.') + print(f'Black: reformatted in {time.time() - start:.4f}s.') def get_configs(): filename = vim.eval("@%") From d85cf00ee80f00b25a819afef7f466dc871fa68d Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:28:43 -0400 Subject: [PATCH 281/700] Remove blib2to3 grammar cache logging (#3193) As error logs are emitted often (they happen when Black's cache directory is created after blib2to3 tries to write its cache) and cause issues to be filed by users who think Black isn't working correctly. These errors are expected for now and aren't a cause for concern so let's remove them to stop worrying users (and new issues from being opened). We can improve the blib2to3 caching mechanism to write its cache at the end of a successful command line invocation later. --- CHANGES.md | 2 ++ src/blib2to3/pgen2/driver.py | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e027b2cae71..a30ac7f25e1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,6 +54,8 @@ - Change from deprecated `asyncio.get_event_loop()` to create our event loop which removes DeprecationWarning (#3164) +- Remove logging from internal `blib2to3` library since it regularily emits error logs + about failed caching that can and should be ignored (#3193) ### Packaging diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 8fe820651da..daf271dfa9a 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -263,14 +263,13 @@ def load_grammar( logger = logging.getLogger(__name__) gp = _generate_pickle_name(gt) if gp is None else gp if force or not _newer(gp, gt): - logger.info("Generating grammar tables from %s", gt) g: grammar.Grammar = pgen.generate_grammar(gt) if save: - logger.info("Writing grammar tables to %s", gp) try: g.dump(gp) - except OSError as e: - logger.info("Writing failed: %s", e) + except OSError: + # Ignore error, caching is not vital. + pass else: g = grammar.Grammar() g.load(gp) From eaa048925e4443cc0e2b57b795f2852bedb4287f Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:38:39 -0400 Subject: [PATCH 282/700] Add sanity check to executable CD + more (#3190) Building executables without any testing is quite sketchy, let's at least verify they won't crash on startup and format Black's own codebase. Also replaced "binaries" with "executables" since it's clearer and won't be confused with mypyc. Finally, I added colorama so all Windows users can get colour. --- .github/workflows/upload_binary.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index ed5ed961e67..22535a64c67 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -1,16 +1,14 @@ -name: Upload self-contained binaries +name: Publish executables on: release: types: [published] permissions: - contents: read + contents: write # actions/upload-release-asset needs this. jobs: build: - permissions: - contents: write # for actions/upload-release-asset to upload release asset runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -38,15 +36,21 @@ jobs: with: python-version: "*" - - name: Install dependencies + - name: Install Black and PyInstaller run: | - python -m pip install --upgrade pip wheel setuptools - python -m pip install . + python -m pip install --upgrade pip wheel + python -m pip install .[colorama] python -m pip install pyinstaller - - name: Build binary + - name: Build executable with PyInstaller + run: > + python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data + 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py + + - name: Quickly test executable run: | - python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py + ./dist/${{ matrix.asset_name }} --version + ./dist/${{ matrix.asset_name }} src --verbose - name: Upload binary as release asset uses: actions/upload-release-asset@v1 From ca0dbb8fa6cca8c1fc2650cde9e71402c03a3324 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 31 Jul 2022 10:34:29 -0400 Subject: [PATCH 283/700] Move fuzz.py to scripts/ (#3199) --- fuzz.py => scripts/fuzz.py | 0 tox.ini | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename fuzz.py => scripts/fuzz.py (100%) diff --git a/fuzz.py b/scripts/fuzz.py similarity index 100% rename from fuzz.py rename to scripts/fuzz.py diff --git a/tox.ini b/tox.ini index 7af9e48d6f0..51ff4872db0 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ deps = commands = pip install -e .[d] coverage erase - coverage run fuzz.py + coverage run {toxinidir}/scripts/fuzz.py coverage report [testenv:run_self] From b776bf92adb7f47cd92f550e33c2db445d226f78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 13:51:46 -0400 Subject: [PATCH 284/700] Bump sphinx from 5.1.0 to 5.1.1 in /docs (#3201) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.0 to 5.1.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.0...v5.1.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... 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 e843a68566a..121df45e6c2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.0 -Sphinx==5.1.0 +Sphinx==5.1.1 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 sphinxcontrib-programoutput==0.17 From f066e3fcae49554118962d2bbd9ec92a9958acf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Intrieri?= <81313286+n-borges@users.noreply.github.com> Date: Tue, 2 Aug 2022 18:01:15 +0200 Subject: [PATCH 285/700] makes install available for all users in docker image (#3202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * makes install available for all users in docker image moves the installation path from /root/.local to a virtualenv. this way we still get the lightweight multistage build without excluding non-root users. * adds changelog entry for docker-image fix A changelog entry has been added under the Integration subheader * changes dockerfile to use the venv activate script we are now using the inbuilt venv activate script, as well as explicitly mentioning the binary location in the entrypoint cmd. Co-authored-by: Nicolò Co-authored-by: Cooper Lees --- CHANGES.md | 2 ++ Dockerfile | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a30ac7f25e1..5b29f20bfff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,8 @@ - Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194) +- Docker: changed to a /opt/venv installation + added to PATH to be available to + non-root users (#3202) ### Output diff --git a/Dockerfile b/Dockerfile index c393e29f632..4e8f12f9798 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,18 @@ FROM python:3-slim AS builder RUN mkdir /src COPY . /src/ -RUN pip install --no-cache-dir --upgrade pip setuptools wheel \ +ENV VIRTUAL_ENV=/opt/venv +RUN python -m venv $VIRTUAL_ENV +RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setuptools wheel \ # Install build tools to compile dependencies that don't have prebuilt wheels && apt update && apt install -y git build-essential \ && cd /src \ - && pip install --user --no-cache-dir .[colorama,d] + && pip install --no-cache-dir .[colorama,d] FROM python:3-slim # copy only Python packages to limit the image size -COPY --from=builder /root/.local /root/.local -ENV PATH=/root/.local/bin:$PATH +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" -CMD ["black"] +CMD ["/opt/venv/bin/black"] From 507234c47d39f5b1d8289cdd49994e03dd97bcb4 Mon Sep 17 00:00:00 2001 From: Tom Fryers <61272761+TomFryers@users.noreply.github.com> Date: Tue, 2 Aug 2022 22:22:04 +0100 Subject: [PATCH 286/700] Remove invalid syntax in docstrings -S --preview test (#3205) uR is not a legal string prefix, so this test breaks (AssertionError: cannot use --safe with this file; failed to parse source file AST: invalid syntax) if changed to one in which the file is changed. I've changed the last test to have u alone, and added an R to the test above instead. --- .../docstring_preview_no_string_normalization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/miscellaneous/docstring_preview_no_string_normalization.py b/tests/data/miscellaneous/docstring_preview_no_string_normalization.py index 0957231eb9c..338cc01d33e 100644 --- a/tests/data/miscellaneous/docstring_preview_no_string_normalization.py +++ b/tests/data/miscellaneous/docstring_preview_no_string_normalization.py @@ -3,8 +3,8 @@ def do_not_touch_this_prefix(): def do_not_touch_this_prefix2(): - F'There was a bug where docstring prefixes would be normalized even with -S.' + FR'There was a bug where docstring prefixes would be normalized even with -S.' def do_not_touch_this_prefix3(): - uR'''There was a bug where docstring prefixes would be normalized even with -S.''' + u'''There was a bug where docstring prefixes would be normalized even with -S.''' From 6064a435453cdba47c43d71f3d0ea1aa19a29206 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 10 Aug 2022 14:29:47 -0700 Subject: [PATCH 287/700] Use debug f-strings for feature detection (#3215) Fixes GH-2907. --- CHANGES.md | 2 ++ src/black/__init__.py | 7 +++++++ src/black/mode.py | 5 +++++ tests/test_black.py | 20 ++++++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5b29f20bfff..1fc8c65d6d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ +- Black now uses the presence of debug f-strings to detect target version. (#3215) + ### Documentation +- `blackd` now supports preview style via `X-Preview` header (#3217) + ### Configuration diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index fc9d1cab716..a2d4252109a 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -54,8 +54,11 @@ The headers controlling how source code is formatted are: command line flag. If present and its value is not the empty string, no string normalization will be performed. - `X-Skip-Magic-Trailing-Comma`: corresponds to the `--skip-magic-trailing-comma` - command line flag. If present and its value is not the empty string, trailing commas + command line flag. If present and its value is not an empty string, trailing commas will not be used as a reason to split lines. +- `X-Preview`: corresponds to the `--preview` command line flag. If present and its + value is not an empty string, experimental and potentially disruptive style changes + will be used. - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the `--fast` command line flag. - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index a6de79fbeaa..e52a9917cf3 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -32,6 +32,7 @@ PYTHON_VARIANT_HEADER = "X-Python-Variant" SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" +PREVIEW = "X-Preview" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" DIFF_HEADER = "X-Diff" @@ -41,6 +42,7 @@ PYTHON_VARIANT_HEADER, SKIP_STRING_NORMALIZATION_HEADER, SKIP_MAGIC_TRAILING_COMMA, + PREVIEW, FAST_OR_SAFE_HEADER, DIFF_HEADER, ] @@ -109,6 +111,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: skip_magic_trailing_comma = bool( request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False) ) + preview = bool(request.headers.get(PREVIEW, False)) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True @@ -118,6 +121,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: line_length=line_length, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, + preview=preview, ) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 18b2c98ac1f..1d12113a3f3 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -167,6 +167,13 @@ async def test_blackd_invalid_line_length(self) -> None: ) self.assertEqual(response.status, 400) + @unittest_run_loop + async def test_blackd_preview(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + ) + self.assertEqual(response.status, 204) + @unittest_run_loop async def test_blackd_response_black_version_header(self) -> None: response = await self.client.post("/") From e7b967132fdbb9e2e4c4e9916530d238848ab183 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 12 Aug 2022 23:33:17 -0400 Subject: [PATCH 290/700] Port & upstream mypyc wheel build workflow (#3197) --- .github/mypyc-requirements.txt | 2 +- .github/workflows/pypi_upload.yml | 56 ++++++++++++++++++++++++++----- pyproject.toml | 52 ++++++++++++++++++++++++++++ setup.py | 5 ++- 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/.github/mypyc-requirements.txt b/.github/mypyc-requirements.txt index 4542673174c..352d36c0070 100644 --- a/.github/mypyc-requirements.txt +++ b/.github/mypyc-requirements.txt @@ -1,4 +1,4 @@ -mypy == 0.920 +mypy == 0.971 # A bunch of packages for type information mypy-extensions >= 0.4.3 diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index cda215aa5d6..31a83266345 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -1,4 +1,4 @@ -name: pypi_upload +name: Publish to PyPI on: release: @@ -8,14 +8,14 @@ permissions: contents: read jobs: - build: - name: PyPI Upload + main: + name: sdist + pure wheel runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python + - name: Set up latest Python uses: actions/setup-python@v4 with: python-version: "*" @@ -26,11 +26,51 @@ jobs: python -m pip install --upgrade build twine - name: Build wheel and source distributions - run: | - python -m build + run: python -m build - name: Upload to PyPI via Twine env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - twine upload --verbose -u '__token__' dist/* + run: twine upload --verbose -u '__token__' dist/* + + mypyc: + name: mypyc wheels (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: linux-x86_64 + - os: windows-2019 + name: windows-amd64 + - os: macos-11 + name: macos-x86_64 + macos_arch: "x86_64" + - os: macos-11 + name: macos-arm64 + macos_arch: "arm64" + - os: macos-11 + name: macos-universal2 + macos_arch: "universal2" + + steps: + - uses: actions/checkout@v3 + + - name: Build wheels via cibuildwheel + uses: pypa/cibuildwheel@v2.8.1 + env: + CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" + # This isn't supported in pyproject.toml which makes sense (but is annoying). + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.6.2" + + - name: Upload wheels as workflow artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.name }}-mypyc-wheels + path: ./wheelhouse/*.whl + + - name: Upload wheels to PyPI via Twine + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: pipx run twine upload --verbose -u '__token__' wheelhouse/*.whl diff --git a/pyproject.toml b/pyproject.toml index 36765072056..813e86b2e93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,58 @@ preview = true requires = ["setuptools>=45.0", "setuptools_scm[toml]>=6.3.1", "wheel"] build-backend = "setuptools.build_meta" +[tool.cibuildwheel] +build-verbosity = 1 +# So these are the environments we target: +# - Python: CPython 3.6+ only +# - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 +# - OS: Linux (no musl), Windows, and macOS +build = "cp3*-*" +skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp-*"] +before-build = ["pip install -r .github/mypyc-requirements.txt"] +# This is the bare minimum needed to run the test suite. Pulling in the full +# test_requirements.txt would download a bunch of other packages not necessary +# here and would slow down the testing step a fair bit. +test-requires = ["pytest>=6.1.1"] +test-command = 'pytest {project} -k "not incompatible_with_mypyc"' +test-extras = ["d"," jupyter"] +# Skip trying to test arm64 builds on Intel Macs. (so cross-compilation doesn't +# straight up crash) +test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"] + +[tool.cibuildwheel.environment] +BLACK_USE_MYPYC = "1" +MYPYC_OPT_LEVEL = "3" +MYPYC_DEBUG_LEVEL = "0" +# The dependencies required to build wheels with mypyc aren't specified in +# [build-system].requires so we'll have to manage the build environment ourselves. +PIP_NO_BUILD_ISOLATION = "no" + +[tool.cibuildwheel.linux] +before-build = [ + "pip install -r .github/mypyc-requirements.txt", + "yum install -y clang", +] +# Newer images break the builds, not sure why. We'll need to investigate more later. +manylinux-x86_64-image = "quay.io/pypa/manylinux2014_x86_64:2021-11-20-f410d11" + +[tool.cibuildwheel.linux.environment] +BLACK_USE_MYPYC = "1" +MYPYC_OPT_LEVEL = "3" +MYPYC_DEBUG_LEVEL = "0" +PIP_NO_BUILD_ISOLATION = "no" +# Black needs Clang to compile successfully on Linux. +CC = "clang" + +[tool.cibuildwheel.windows] +# For some reason, (compiled) mypyc is failing to start up with "ImportError: DLL load +# failed: A dynamic link library (DLL) initialization routine failed." on Windows for +# at least 3.6. Let's just use interpreted mypy[c]. +# See also: https://github.com/mypyc/mypyc/issues/819. +before-build = [ + "pip install -r .github/mypyc-requirements.txt --no-binary mypy" +] + [tool.isort] atomic = true profile = "black" diff --git a/setup.py b/setup.py index 3accdf433bc..bc0cc32352e 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,10 @@ def find_python_files(base: Path) -> List[Path]: ] opt_level = os.getenv("MYPYC_OPT_LEVEL", "3") - ext_modules = mypycify(mypyc_targets, opt_level=opt_level, verbose=True) + debug_level = os.getenv("MYPYC_DEBUG_LEVEL", "3") + ext_modules = mypycify( + mypyc_targets, opt_level=opt_level, debug_level=debug_level, verbose=True + ) else: ext_modules = [] From 4ebf14d17ed544be893be5706c02116fd8b83b4c Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 13 Aug 2022 06:41:34 -0700 Subject: [PATCH 291/700] Strip trailing commas in subscripts with -C (#3209) Fixes #2296, #3204 --- CHANGES.md | 2 ++ src/black/lines.py | 18 +++++++++- src/black/mode.py | 1 + .../data/preview/skip_magic_trailing_comma.py | 34 +++++++++++++++++++ tests/test_format.py | 7 +++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/data/preview/skip_magic_trailing_comma.py diff --git a/CHANGES.md b/CHANGES.md index 79f4ce59187..fb7a2723b67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,8 @@ this is invalid. This was a bug introduced in version 22.6.0. (#3166) - `--skip-string-normalization` / `-S` now prevents docstring prefixes from being normalized as expected (#3168) +- When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from + subscript expressions with more than 1 element (#3209) ### _Blackd_ diff --git a/src/black/lines.py b/src/black/lines.py index 1ebc7808901..30622650d53 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -273,6 +273,8 @@ def has_magic_trailing_comma( - it's not a single-element subscript Additionally, if ensure_removable: - it's not from square bracket indexing + (specifically, single-element square bracket indexing with + Preview.skip_magic_trailing_comma_in_subscript) """ if not ( closing.type in CLOSING_BRACKETS @@ -301,8 +303,22 @@ def has_magic_trailing_comma( if not ensure_removable: return True + comma = self.leaves[-1] - return bool(comma.parent and comma.parent.type == syms.listmaker) + if comma.parent is None: + return False + if Preview.skip_magic_trailing_comma_in_subscript in self.mode: + return ( + comma.parent.type != syms.subscriptlist + or closing.opening_bracket is None + or not is_one_sequence_between( + closing.opening_bracket, + closing, + self.leaves, + brackets=(token.LSQB, token.RSQB), + ) + ) + return comma.parent.type == syms.listmaker if self.is_import: return True diff --git a/src/black/mode.py b/src/black/mode.py index f6d0cbf62bd..6c0847e8bcc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -156,6 +156,7 @@ class Preview(Enum): remove_block_trailing_newline = auto() remove_redundant_parens = auto() string_processing = auto() + skip_magic_trailing_comma_in_subscript = auto() class Deprecated(UserWarning): diff --git a/tests/data/preview/skip_magic_trailing_comma.py b/tests/data/preview/skip_magic_trailing_comma.py new file mode 100644 index 00000000000..e98174af427 --- /dev/null +++ b/tests/data/preview/skip_magic_trailing_comma.py @@ -0,0 +1,34 @@ +# We should not remove the trailing comma in a single-element subscript. +a: tuple[int,] +b = tuple[int,] + +# But commas in multiple element subscripts should be removed. +c: tuple[int, int,] +d = tuple[int, int,] + +# Remove commas for non-subscripts. +small_list = [1,] +list_of_types = [tuple[int,],] +small_set = {1,} +set_of_types = {tuple[int,],} + +# Except single element tuples +small_tuple = (1,) + +# output +# We should not remove the trailing comma in a single-element subscript. +a: tuple[int,] +b = tuple[int,] + +# But commas in multiple element subscripts should be removed. +c: tuple[int, int] +d = tuple[int, int] + +# Remove commas for non-subscripts. +small_list = [1] +list_of_types = [tuple[int,]] +small_set = {1} +set_of_types = {tuple[int,]} + +# Except single element tuples +small_tuple = (1,) diff --git a/tests/test_format.py b/tests/test_format.py index 3645934721f..01cd61eef63 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -36,7 +36,12 @@ def test_simple_format(filename: str) -> None: @pytest.mark.parametrize("filename", all_data_cases("preview")) def test_preview_format(filename: str) -> None: - check_file("preview", filename, black.Mode(preview=True)) + magic_trailing_comma = filename != "skip_magic_trailing_comma" + check_file( + "preview", + filename, + black.Mode(preview=True, magic_trailing_comma=magic_trailing_comma), + ) @pytest.mark.parametrize("filename", all_data_cases("preview_39")) From 6e0ad52e7a30771d0056fa60bfe5e368f2bc2417 Mon Sep 17 00:00:00 2001 From: Alexander Huynh Date: Sat, 20 Aug 2022 13:45:20 -0400 Subject: [PATCH 292/700] Update email (#3235) This file gets scraped a lot, so create a distinct email for potential spam. --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index f30cd55a08b..c81bc024e1d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -19,7 +19,7 @@ Multiple contributions by: - [Abdur-Rahmaan Janhangeer](mailto:arj.python@gmail.com) - [Adam Johnson](mailto:me@adamj.eu) - [Adam Williamson](mailto:adamw@happyassassin.net) -- [Alexander Huynh](mailto:github@grande.coffee) +- [Alexander Huynh](mailto:ahrex-gh-psf-black@e.sc) - [Alexandr Artemyev](mailto:mogost@gmail.com) - [Alex Vandiver](mailto:github@chmrr.net) - [Allan Simon](mailto:allan.simon@supinfo.com) From 59acf8af38a72e57b26d739adb5d5e7f350e8f2c Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 22 Aug 2022 20:39:48 -0700 Subject: [PATCH 293/700] Add passing 3.11 CI by exempting blackd tests (#3234) - Had to exempt blackd tests for now due to aiohttp - Skip by using `sys.version_info` tuple - aiohttp does not compile in 3.11 yet - refer to #3230 - Add a deadsnakes ubuntu workflow to run 3.11-dev to ensure we don't regress - Have it also format ourselves Test: - `tox -e 311` Co-authored-by: Cooper Ry Lees Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/workflows/test-311.yml | 57 +++++ CHANGES.md | 2 + setup.py | 1 + tests/test_blackd.py | 379 +++++++++++++++++---------------- tox.ini | 27 ++- 5 files changed, 279 insertions(+), 187 deletions(-) create mode 100644 .github/workflows/test-311.yml diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml new file mode 100644 index 00000000000..e23a67e89eb --- /dev/null +++ b/.github/workflows/test-311.yml @@ -0,0 +1,57 @@ +name: Partially test 3.11 dev + +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + + pull_request: + paths-ignore: + - "docs/**" + - "*.md" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + main: + # 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.11.0-rc - 3.11"] + os: [ubuntu-latest, macOS-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Run tests via tox + run: | + python -m tox -e 311 + + - name: Format ourselves + run: | + python -m pip install . + python -m black --check src/ diff --git a/CHANGES.md b/CHANGES.md index fb7a2723b67..cae232684bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,8 @@ +- Python 3.11 is now supported, except for `blackd` (#3234) + ### Parser diff --git a/setup.py b/setup.py index bc0cc32352e..2cf455573c9 100644 --- a/setup.py +++ b/setup.py @@ -127,6 +127,7 @@ def find_python_files(base: Path) -> List[Path]: "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 1d12113a3f3..8e739063f6e 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,4 +1,5 @@ import re +import sys from typing import Any from unittest.mock import patch @@ -7,195 +8,201 @@ from tests.util import DETERMINISTIC_HEADER, read_data -try: - from aiohttp import web - from aiohttp.test_utils import AioHTTPTestCase - - import blackd -except ImportError as e: - raise RuntimeError("Please install Black with the 'd' extra") from e - -try: - from aiohttp.test_utils import unittest_run_loop -except ImportError: - # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and aiohttp 4 - # removed it. To maintain compatibility we can make our own no-op decorator. - def unittest_run_loop(func: Any, *args: Any, **kwargs: Any) -> Any: - return func - - -@pytest.mark.blackd -class BlackDTestCase(AioHTTPTestCase): - def test_blackd_main(self) -> None: - with patch("blackd.web.run_app"): - result = CliRunner().invoke(blackd.main, []) - if result.exception is not None: - raise result.exception - self.assertEqual(result.exit_code, 0) - - async def get_application(self) -> web.Application: - return blackd.make_app() - - @unittest_run_loop - async def test_blackd_request_needs_formatting(self) -> None: - response = await self.client.post("/", data=b"print('hello world')") - self.assertEqual(response.status, 200) - self.assertEqual(response.charset, "utf8") - self.assertEqual(await response.read(), b'print("hello world")\n') - - @unittest_run_loop - async def test_blackd_request_no_change(self) -> None: - response = await self.client.post("/", data=b'print("hello world")\n') - self.assertEqual(response.status, 204) - self.assertEqual(await response.read(), b"") - - @unittest_run_loop - async def test_blackd_request_syntax_error(self) -> None: - response = await self.client.post("/", data=b"what even ( is") - self.assertEqual(response.status, 400) - content = await response.text() - self.assertTrue( - content.startswith("Cannot parse"), - msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", - ) - - @unittest_run_loop - async def test_blackd_unsupported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} - ) - self.assertEqual(response.status, 501) - - @unittest_run_loop - async def test_blackd_supported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_python_variant(self) -> None: - async def check(header_value: str, expected_status: int = 400) -> None: +LESS_THAN_311 = sys.version_info < (3, 11) + +if LESS_THAN_311: # noqa: C901 + try: + from aiohttp import web + from aiohttp.test_utils import AioHTTPTestCase + + import blackd + except ImportError as e: + raise RuntimeError("Please install Black with the 'd' extra") from e + + try: + from aiohttp.test_utils import unittest_run_loop + except ImportError: + # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and aiohttp 4 + # removed it. To maintain compatibility we can make our own no-op decorator. + def unittest_run_loop(func: Any, *args: Any, **kwargs: Any) -> Any: + return func + + @pytest.mark.blackd + class BlackDTestCase(AioHTTPTestCase): + def test_blackd_main(self) -> None: + with patch("blackd.web.run_app"): + result = CliRunner().invoke(blackd.main, []) + if result.exception is not None: + raise result.exception + self.assertEqual(result.exit_code, 0) + + async def get_application(self) -> web.Application: + return blackd.make_app() + + @unittest_run_loop + async def test_blackd_request_needs_formatting(self) -> None: + response = await self.client.post("/", data=b"print('hello world')") + self.assertEqual(response.status, 200) + self.assertEqual(response.charset, "utf8") + self.assertEqual(await response.read(), b'print("hello world")\n') + + @unittest_run_loop + async def test_blackd_request_no_change(self) -> None: + response = await self.client.post("/", data=b'print("hello world")\n') + self.assertEqual(response.status, 204) + self.assertEqual(await response.read(), b"") + + @unittest_run_loop + async def test_blackd_request_syntax_error(self) -> None: + response = await self.client.post("/", data=b"what even ( is") + self.assertEqual(response.status, 400) + content = await response.text() + self.assertTrue( + content.startswith("Cannot parse"), + msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", + ) + + @unittest_run_loop + async def test_blackd_unsupported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} + ) + self.assertEqual(response.status, 501) + + @unittest_run_loop + async def test_blackd_supported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} + ) + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_python_variant(self) -> None: + async def check(header_value: str, expected_status: int = 400) -> None: + response = await self.client.post( + "/", + data=b"what", + headers={blackd.PYTHON_VARIANT_HEADER: header_value}, + ) + self.assertEqual(response.status, expected_status) + + await check("lol") + await check("ruby3.5") + await check("pyi3.6") + await check("py1.5") + await check("2") + await check("2.7") + await check("py2.7") + await check("2.8") + await check("py2.8") + await check("3.0") + await check("pypy3.0") + await check("jython3.4") + + @unittest_run_loop + async def test_blackd_pyi(self) -> None: + source, expected = read_data("miscellaneous", "stub.pyi") + response = await self.client.post( + "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} + ) + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), expected) + + @unittest_run_loop + async def test_blackd_diff(self) -> None: + diff_header = re.compile( + r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + + source, _ = read_data("miscellaneous", "blackd_diff") + expected, _ = read_data("miscellaneous", "blackd_diff.diff") + response = await self.client.post( - "/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value} + "/", data=source, headers={blackd.DIFF_HEADER: "true"} ) - self.assertEqual(response.status, expected_status) - - await check("lol") - await check("ruby3.5") - await check("pyi3.6") - await check("py1.5") - await check("2") - await check("2.7") - await check("py2.7") - await check("2.8") - await check("py2.8") - await check("3.0") - await check("pypy3.0") - await check("jython3.4") - - @unittest_run_loop - async def test_blackd_pyi(self) -> None: - source, expected = read_data("miscellaneous", "stub.pyi") - response = await self.client.post( - "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} - ) - self.assertEqual(response.status, 200) - self.assertEqual(await response.text(), expected) - - @unittest_run_loop - async def test_blackd_diff(self) -> None: - diff_header = re.compile( - r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" - ) - - source, _ = read_data("miscellaneous", "blackd_diff") - expected, _ = read_data("miscellaneous", "blackd_diff.diff") - - response = await self.client.post( - "/", data=source, headers={blackd.DIFF_HEADER: "true"} - ) - self.assertEqual(response.status, 200) - - actual = await response.text() - actual = diff_header.sub(DETERMINISTIC_HEADER, actual) - self.assertEqual(actual, expected) - - @unittest_run_loop - async def test_blackd_python_variant(self) -> None: - code = ( - "def f(\n" - " and_has_a_bunch_of,\n" - " very_long_arguments_too,\n" - " and_lots_of_them_as_well_lol,\n" - " **and_very_long_keyword_arguments\n" - "):\n" - " pass\n" - ) - - async def check(header_value: str, expected_status: int) -> None: + self.assertEqual(response.status, 200) + + actual = await response.text() + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + + @unittest_run_loop + async def test_blackd_python_variant(self) -> None: + code = ( + "def f(\n" + " and_has_a_bunch_of,\n" + " very_long_arguments_too,\n" + " and_lots_of_them_as_well_lol,\n" + " **and_very_long_keyword_arguments\n" + "):\n" + " pass\n" + ) + + async def check(header_value: str, expected_status: int) -> None: + response = await self.client.post( + "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} + ) + self.assertEqual( + response.status, expected_status, msg=await response.text() + ) + + await check("3.6", 200) + await check("py3.6", 200) + await check("3.6,3.7", 200) + await check("3.6,py3.7", 200) + await check("py36,py37", 200) + await check("36", 200) + await check("3.6.4", 200) + await check("3.4", 204) + await check("py3.4", 204) + await check("py34,py36", 204) + await check("34", 204) + + @unittest_run_loop + async def test_blackd_line_length(self) -> None: response = await self.client.post( - "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} ) - self.assertEqual( - response.status, expected_status, msg=await response.text() + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_line_length(self) -> None: + response = await self.client.post( + "/", + data=b'print("hello")\n', + headers={blackd.LINE_LENGTH_HEADER: "NaN"}, ) + self.assertEqual(response.status, 400) - await check("3.6", 200) - await check("py3.6", 200) - await check("3.6,3.7", 200) - await check("3.6,py3.7", 200) - await check("py36,py37", 200) - await check("36", 200) - await check("3.6.4", 200) - await check("3.4", 204) - await check("py3.4", 204) - await check("py34,py36", 204) - await check("34", 204) - - @unittest_run_loop - async def test_blackd_line_length(self) -> None: - response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_line_length(self) -> None: - response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "NaN"} - ) - self.assertEqual(response.status, 400) - - @unittest_run_loop - async def test_blackd_preview(self) -> None: - response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} - ) - self.assertEqual(response.status, 204) - - @unittest_run_loop - async def test_blackd_response_black_version_header(self) -> None: - response = await self.client.post("/") - self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) - - @unittest_run_loop - async def test_cors_preflight(self) -> None: - response = await self.client.options( - "/", - headers={ - "Access-Control-Request-Method": "POST", - "Origin": "*", - "Access-Control-Request-Headers": "Content-Type", - }, - ) - self.assertEqual(response.status, 200) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) - - @unittest_run_loop - async def test_cors_headers_present(self) -> None: - response = await self.client.post("/", headers={"Origin": "*"}) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) + @unittest_run_loop + async def test_blackd_preview(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + ) + self.assertEqual(response.status, 204) + + @unittest_run_loop + async def test_blackd_response_black_version_header(self) -> None: + response = await self.client.post("/") + self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) + + @unittest_run_loop + async def test_cors_preflight(self) -> None: + response = await self.client.options( + "/", + headers={ + "Access-Control-Request-Method": "POST", + "Origin": "*", + "Access-Control-Request-Headers": "Content-Type", + }, + ) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) + + @unittest_run_loop + async def test_cors_headers_present(self) -> None: + response = await self.client.post("/", headers={"Origin": "*"}) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) diff --git a/tox.ini b/tox.ini index 51ff4872db0..5f3874c23b4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310,py3},fuzz,run_self +envlist = {,ci-}py{36,37,38,39,310,311,py3},fuzz,run_self [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -50,6 +50,31 @@ commands = --cov --cov-append {posargs} coverage report +[testenv:{,ci-}311] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +recreate = True +deps = + -r{toxinidir}/test_requirements.txt +; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 +; this seems to cause tox to wait forever +; remove this when pypy releases the bugfix +commands = + pip install -e . + coverage erase + pytest tests \ + --run-optional no_jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov {posargs} + pip install -e .[jupyter] + pytest tests --run-optional jupyter \ + -m jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov --cov-append {posargs} + coverage report + [testenv:fuzz] skip_install = True deps = From 21218b666aeafd1c089cbe998e730f97605d25b2 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Mon, 22 Aug 2022 20:40:38 -0700 Subject: [PATCH 294/700] Fix a string merging/split issue caused by standalone comments. (#3227) Fixes #2734: a standalone comment causes strings to be merged into one far too long (and requiring two passes to do so). Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/workflows/diff_shades.yml | 1 - CHANGES.md | 2 ++ src/black/trans.py | 16 +++++++++++++- tests/data/preview/long_strings.py | 35 ++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 390089eca42..fef9637c92f 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -155,4 +155,3 @@ jobs: if: always() run: > diff-shades show-failed --check --show-log ${{ matrix.target-analysis }} - --check-allow 'sqlalchemy:test/orm/test_relationship_criteria.py' diff --git a/CHANGES.md b/CHANGES.md index cae232684bd..39db0fb95b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ normalized as expected (#3168) - When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from subscript expressions with more than 1 element (#3209) +- Fix a string merging/split issue when a comment is present in the middle of implicitly + concatenated strings on its own line (#3227) ### _Blackd_ diff --git a/src/black/trans.py b/src/black/trans.py index dc9c5520d5b..9e0284cefe3 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -553,6 +553,9 @@ def make_naked(string: str, string_prefix: str) -> str: next_str_idx += 1 + # Take a note on the index of the non-STRING leaf. + non_string_idx = next_str_idx + S_leaf = Leaf(token.STRING, S) if self.normalize_strings: S_leaf.value = normalize_string_quotes(S_leaf.value) @@ -572,7 +575,18 @@ def make_naked(string: str, string_prefix: str) -> str: string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, "")) if atom_node is not None: - replace_child(atom_node, string_leaf) + # If not all children of the atom node are merged (this can happen + # when there is a standalone comment in the middle) ... + if non_string_idx - string_idx < len(atom_node.children): + # We need to replace the old STRING leaves with the new string leaf. + first_child_idx = LL[string_idx].remove() + for idx in range(string_idx + 1, non_string_idx): + LL[idx].remove() + if first_child_idx is not None: + atom_node.insert_child(first_child_idx, string_leaf) + else: + # Else replace the atom node with the new string leaf. + replace_child(atom_node, string_leaf) # Build the final line ('new_line') that this method will later return. new_line = line.clone() diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 430f760cf0b..26400eea450 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -72,6 +72,25 @@ zzz, ) +inline_comments_func1( + "if there are inline " + "comments in the middle " + # Here is the standard alone comment. + "of the implicitly concatenated " + "string, we should handle " + "them correctly", + xxx, +) + +inline_comments_func2( + "what if the string is very very very very very very very very very very long and this part does " + "not fit into a single line? " + # Here is the standard alone comment. + "then the string should still be properly handled by merging and splitting " + "it into parts that fit in line length.", + xxx, +) + raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format("method calls") @@ -395,6 +414,22 @@ def foo(): zzz, ) +inline_comments_func1( + "if there are inline comments in the middle " + # Here is the standard alone comment. + "of the implicitly concatenated string, we should handle them correctly", + xxx, +) + +inline_comments_func2( + "what if the string is very very very very very very very very very very long and" + " this part does not fit into a single line? " + # Here is the standard alone comment. + "then the string should still be properly handled by merging and splitting " + "it into parts that fit in line length.", + xxx, +) + raw_string = ( r"This is a long raw string. When re-formatting this string, black needs to make" r" sure it prepends the 'r' onto the new string." From a5fde8ab9be26221d01bdd0a426db07cdb6f0f04 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 26 Aug 2022 15:45:31 -0400 Subject: [PATCH 295/700] Remove hacky subprocess call in action.yml (#3226) Updates action.yml to use the alternative $GITHUB_ACTION_PATH variable instead of the original ${{ github.action_path }} which caused issues with bash on the Windows runners. This removes the need for a Python subprocess to call the main.py script. --- AUTHORS.md | 1 + action.yml | 19 ++----------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index c81bc024e1d..533606240d3 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -79,6 +79,7 @@ Multiple contributions by: - [Hugo Barrera](mailto::hugo@barrera.io) - Hugo van Kemenade - [Hynek Schlawack](mailto:hs@ox.cx) +- [Ionite](mailto:dev@ionite.io) - [Ivan Katanić](mailto:ivan.katanic@gmail.com) - [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) - [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) diff --git a/action.yml b/action.yml index dbd8ef69ec2..cfa6ef9fb7e 100644 --- a/action.yml +++ b/action.yml @@ -29,25 +29,10 @@ runs: using: composite steps: - run: | - # Exists since using github.action_path + path to main script doesn't work because bash - # interprets the backslashes in github.action_path (which are used when the runner OS - # is Windows) destroying the path to the target file. - # - # Also semicolons are necessary because I can't get the newlines to work - entrypoint="import sys; - import subprocess; - from pathlib import Path; - - MAIN_SCRIPT = Path(r'${GITHUB_ACTION_PATH}') / 'action' / 'main.py'; - - proc = subprocess.run([sys.executable, str(MAIN_SCRIPT)]); - sys.exit(proc.returncode) - " - if [ "$RUNNER_OS" == "Windows" ]; then - echo $entrypoint | python + python $GITHUB_ACTION_PATH/action/main.py else - echo $entrypoint | python3 + python3 $GITHUB_ACTION_PATH/action/main.py fi env: # TODO: Remove once https://github.com/actions/runner/issues/665 is fixed. From c47b91f513052cd39b818ea7c19716423c85c04e Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:07:25 -0700 Subject: [PATCH 296/700] Fix misdetection of project root with `--stdin-filename` (#3216) There are a number of places this behaviour could be patched, for instance, it's quite tempting to patch it in `get_sources`. However I believe we generally have the invariant that project root contains all files we want to format, in which case it seems prudent to keep that invariant. This also improves the accuracy of the "sources to be formatted" log message with --stdin-filename. Fixes GH-3207. --- CHANGES.md | 2 ++ src/black/__init__.py | 8 ++++++-- src/black/files.py | 6 +++++- tests/test_black.py | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39db0fb95b8..17659522fd1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,8 @@ - Black now uses the presence of debug f-strings to detect target version. (#3215) +- Fix misdetection of project root and verbose logging of sources in cases involving + `--stdin-filename` (#3216) ### Documentation diff --git a/src/black/__init__.py b/src/black/__init__.py index b8a9d031896..a0c1ad4b416 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -469,7 +469,9 @@ def main( # noqa: C901 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) + root, method = ( + find_project_root(src, stdin_filename) if code is None else (None, None) + ) ctx.obj["root"] = root if verbose: @@ -480,7 +482,9 @@ def main( # noqa: C901 ) normalized = [ - (normalize_path_maybe_ignore(Path(source), root), source) + (source, source) + if source == "-" + else (normalize_path_maybe_ignore(Path(source), root), source) for source in src ] srcs_string = ", ".join( diff --git a/src/black/files.py b/src/black/files.py index 17515d52b57..d51c1bc7a90 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -39,7 +39,9 @@ @lru_cache() -def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]: +def find_project_root( + srcs: Sequence[str], stdin_filename: Optional[str] = None +) -> Tuple[Path, str]: """Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories @@ -52,6 +54,8 @@ def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]: the second element as a string describing the method by which the project root was discovered. """ + if stdin_filename is not None: + srcs = tuple(stdin_filename if s == "-" else s for s in srcs) if not srcs: srcs = [str(Path.cwd().resolve())] diff --git a/tests/test_black.py b/tests/test_black.py index 81e7a9a7d0d..c76b3faddf5 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1396,6 +1396,12 @@ def test_find_project_root(self) -> None: (src_dir.resolve(), "pyproject.toml"), ) + with change_directory(test_dir): + self.assertEqual( + black.find_project_root(("-",), stdin_filename="../src/a.py"), + (src_dir.resolve(), "pyproject.toml"), + ) + @patch( "black.files.find_user_pyproject_toml", ) From e269f44b25737360e0dc65379f889dfa931dc68a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 3 Aug 2022 20:18:33 -0400 Subject: [PATCH 297/700] Lazily import parallelized format modules `black.reformat_many` depends on a lot of slow-to-import modules. When formatting simply a single file, the time paid to import those modules is totally wasted. So I moved `black.reformat_many` and its helpers to `black.concurrency` which is now *only* imported if there's more than one file to reformat. This way, running Black over a single file is snappier Here are the numbers before and after this patch running `python -m black --version`: - interpreted: 411 ms +- 9 ms -> 342 ms +- 7 ms: 1.20x faster - compiled: 365 ms +- 15 ms -> 304 ms +- 7 ms: 1.20x faster Co-authored-by: Fabio Zadrozny --- CHANGES.md | 2 + .../reference/reference_functions.rst | 4 +- src/black/__init__.py | 153 ++---------------- src/black/concurrency.py | 145 ++++++++++++++++- tests/test_black.py | 6 +- 5 files changed, 165 insertions(+), 145 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 17659522fd1..34c54710775 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -87,6 +87,8 @@ +- Reduce Black's startup time when formatting a single file by 15-30% (#3211) + ## 22.6.0 ### Style diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index 01ffe44ef53..50eaeb31e15 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -52,7 +52,7 @@ Formatting .. autofunction:: black.reformat_one -.. autofunction:: black.schedule_formatting +.. autofunction:: black.concurrency.schedule_formatting File operations --------------- @@ -173,7 +173,7 @@ Utilities .. autofunction:: black.linegen.should_split_line -.. autofunction:: black.shutdown +.. autofunction:: black.concurrency.shutdown .. autofunction:: black.strings.sub_twice diff --git a/src/black/__init__.py b/src/black/__init__.py index a0c1ad4b416..afc76e1fa0c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,10 +1,8 @@ -import asyncio import io import json import os import platform import re -import signal import sys import tokenize import traceback @@ -13,10 +11,8 @@ from datetime import datetime from enum import Enum from json.decoder import JSONDecodeError -from multiprocessing import Manager, freeze_support from pathlib import Path from typing import ( - TYPE_CHECKING, Any, Dict, Generator, @@ -32,15 +28,19 @@ Union, ) +if sys.version_info >= (3, 8): + from typing import Final +else: + from typing_extensions import Final + import click from click.core import ParameterSource from mypy_extensions import mypyc_attr from pathspec.patterns.gitwildmatch import GitWildMatchPatternError from _black_version import version as __version__ -from black.cache import Cache, filter_cached, get_cache_info, read_cache, write_cache +from black.cache import Cache, get_cache_info, read_cache, write_cache from black.comments import normalize_fmt_off -from black.concurrency import cancel, maybe_install_uvloop, shutdown from black.const import ( DEFAULT_EXCLUDES, DEFAULT_INCLUDES, @@ -91,10 +91,8 @@ from blib2to3.pgen2 import token from blib2to3.pytree import Leaf, Node -if TYPE_CHECKING: - from concurrent.futures import Executor - COMPILED = Path(__file__).suffix in (".pyd", ".so") +DEFAULT_WORKERS: Final = os.cpu_count() # types FileContent = str @@ -125,8 +123,6 @@ def from_configuration( # Legacy name, left for integrations. FileMode = Mode -DEFAULT_WORKERS = os.cpu_count() - def read_pyproject_toml( ctx: click.Context, param: click.Parameter, value: Optional[str] @@ -592,6 +588,8 @@ def main( # noqa: C901 report=report, ) else: + from black.concurrency import reformat_many + reformat_many( sources=sources, fast=fast, @@ -776,132 +774,6 @@ def reformat_one( report.failed(src, str(exc)) -# diff-shades depends on being to monkeypatch this function to operate. I know it's -# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 -@mypyc_attr(patchable=True) -def reformat_many( - sources: Set[Path], - fast: bool, - write_back: WriteBack, - mode: Mode, - report: "Report", - workers: Optional[int], -) -> None: - """Reformat multiple files using a ProcessPoolExecutor.""" - from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor - - executor: Executor - worker_count = workers if workers is not None else DEFAULT_WORKERS - if sys.platform == "win32": - # Work around https://bugs.python.org/issue26903 - assert worker_count is not None - worker_count = min(worker_count, 60) - try: - executor = ProcessPoolExecutor(max_workers=worker_count) - except (ImportError, NotImplementedError, OSError): - # we arrive here if the underlying system does not support multi-processing - # like in AWS Lambda or Termux, in which case we gracefully fallback to - # a ThreadPoolExecutor with just a single worker (more workers would not do us - # any good due to the Global Interpreter Lock) - executor = ThreadPoolExecutor(max_workers=1) - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - loop.run_until_complete( - schedule_formatting( - sources=sources, - fast=fast, - write_back=write_back, - mode=mode, - report=report, - loop=loop, - executor=executor, - ) - ) - finally: - try: - shutdown(loop) - finally: - asyncio.set_event_loop(None) - if executor is not None: - executor.shutdown() - - -async def schedule_formatting( - sources: Set[Path], - fast: bool, - write_back: WriteBack, - mode: Mode, - report: "Report", - loop: asyncio.AbstractEventLoop, - executor: "Executor", -) -> None: - """Run formatting of `sources` in parallel using the provided `executor`. - - (Use ProcessPoolExecutors for actual parallelism.) - - `write_back`, `fast`, and `mode` options are passed to - :func:`format_file_in_place`. - """ - cache: Cache = {} - if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - cache = read_cache(mode) - sources, cached = filter_cached(cache, sources) - for src in sorted(cached): - report.done(src, Changed.CACHED) - if not sources: - return - - cancelled = [] - sources_to_cache = [] - lock = None - if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - # For diff output, we need locks to ensure we don't interleave output - # from different processes. - manager = Manager() - lock = manager.Lock() - tasks = { - asyncio.ensure_future( - loop.run_in_executor( - executor, format_file_in_place, src, fast, mode, write_back, lock - ) - ): src - for src in sorted(sources) - } - pending = tasks.keys() - try: - loop.add_signal_handler(signal.SIGINT, cancel, pending) - loop.add_signal_handler(signal.SIGTERM, cancel, pending) - except NotImplementedError: - # There are no good alternatives for these on Windows. - pass - while pending: - done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) - for task in done: - src = tasks.pop(task) - if task.cancelled(): - cancelled.append(task) - elif task.exception(): - report.failed(src, str(task.exception())) - else: - changed = Changed.YES if task.result() else Changed.NO - # If the file was written back or was successfully checked as - # well-formatted, store this information in the cache. - if write_back is WriteBack.YES or ( - write_back is WriteBack.CHECK and changed is Changed.NO - ): - sources_to_cache.append(src) - report.done(src, changed) - if cancelled: - if sys.version_info >= (3, 7): - await asyncio.gather(*cancelled, return_exceptions=True) - else: - await asyncio.gather(*cancelled, loop=loop, return_exceptions=True) - if sources_to_cache: - write_cache(cache, sources_to_cache, mode) - - def format_file_in_place( src: Path, fast: bool, @@ -1506,8 +1378,11 @@ def patch_click() -> None: def patched_main() -> None: - maybe_install_uvloop() - freeze_support() + if sys.platform == "win32" and getattr(sys, "frozen", False): + from multiprocessing import freeze_support + + freeze_support() + patch_click() main() diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 24f67b62f06..d77ea40bd46 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -1,9 +1,25 @@ +""" +Formatting many files at once via multiprocessing. Contains entrypoint and utilities. + +NOTE: this module is only imported if we need to format several files at once. +""" + import asyncio import logging +import signal import sys -from typing import Any, Iterable +from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor +from multiprocessing import Manager +from pathlib import Path +from typing import Any, Iterable, Optional, Set + +from mypy_extensions import mypyc_attr +from black import DEFAULT_WORKERS, WriteBack, format_file_in_place +from black.cache import Cache, filter_cached, read_cache, write_cache +from black.mode import Mode from black.output import err +from black.report import Changed, Report def maybe_install_uvloop() -> None: @@ -11,7 +27,6 @@ def maybe_install_uvloop() -> None: This is called only from command-line entry points to avoid interfering with the parent process if Black is used as a library. - """ try: import uvloop @@ -55,3 +70,129 @@ def shutdown(loop: asyncio.AbstractEventLoop) -> None: cf_logger = logging.getLogger("concurrent.futures") cf_logger.setLevel(logging.CRITICAL) loop.close() + + +# diff-shades depends on being to monkeypatch this function to operate. I know it's +# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 +@mypyc_attr(patchable=True) +def reformat_many( + sources: Set[Path], + fast: bool, + write_back: WriteBack, + mode: Mode, + report: Report, + workers: Optional[int], +) -> None: + """Reformat multiple files using a ProcessPoolExecutor.""" + maybe_install_uvloop() + + executor: Executor + worker_count = workers if workers is not None else DEFAULT_WORKERS + if sys.platform == "win32": + # Work around https://bugs.python.org/issue26903 + assert worker_count is not None + worker_count = min(worker_count, 60) + try: + executor = ProcessPoolExecutor(max_workers=worker_count) + except (ImportError, NotImplementedError, OSError): + # we arrive here if the underlying system does not support multi-processing + # like in AWS Lambda or Termux, in which case we gracefully fallback to + # a ThreadPoolExecutor with just a single worker (more workers would not do us + # any good due to the Global Interpreter Lock) + executor = ThreadPoolExecutor(max_workers=1) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete( + schedule_formatting( + sources=sources, + fast=fast, + write_back=write_back, + mode=mode, + report=report, + loop=loop, + executor=executor, + ) + ) + finally: + try: + shutdown(loop) + finally: + asyncio.set_event_loop(None) + if executor is not None: + executor.shutdown() + + +async def schedule_formatting( + sources: Set[Path], + fast: bool, + write_back: WriteBack, + mode: Mode, + report: "Report", + loop: asyncio.AbstractEventLoop, + executor: "Executor", +) -> None: + """Run formatting of `sources` in parallel using the provided `executor`. + + (Use ProcessPoolExecutors for actual parallelism.) + + `write_back`, `fast`, and `mode` options are passed to + :func:`format_file_in_place`. + """ + cache: Cache = {} + if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): + cache = read_cache(mode) + sources, cached = filter_cached(cache, sources) + for src in sorted(cached): + report.done(src, Changed.CACHED) + if not sources: + return + + cancelled = [] + sources_to_cache = [] + lock = None + if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): + # For diff output, we need locks to ensure we don't interleave output + # from different processes. + manager = Manager() + lock = manager.Lock() + tasks = { + asyncio.ensure_future( + loop.run_in_executor( + executor, format_file_in_place, src, fast, mode, write_back, lock + ) + ): src + for src in sorted(sources) + } + pending = tasks.keys() + try: + loop.add_signal_handler(signal.SIGINT, cancel, pending) + loop.add_signal_handler(signal.SIGTERM, cancel, pending) + except NotImplementedError: + # There are no good alternatives for these on Windows. + pass + while pending: + done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + for task in done: + src = tasks.pop(task) + if task.cancelled(): + cancelled.append(task) + elif task.exception(): + report.failed(src, str(task.exception())) + else: + changed = Changed.YES if task.result() else Changed.NO + # If the file was written back or was successfully checked as + # well-formatted, store this information in the cache. + if write_back is WriteBack.YES or ( + write_back is WriteBack.CHECK and changed is Changed.NO + ): + sources_to_cache.append(src) + report.done(src, changed) + if cancelled: + if sys.version_info >= (3, 7): + await asyncio.gather(*cancelled, return_exceptions=True) + else: + await asyncio.gather(*cancelled, loop=loop, return_exceptions=True) + if sources_to_cache: + write_cache(cache, sources_to_cache, mode) diff --git a/tests/test_black.py b/tests/test_black.py index c76b3faddf5..5da247b71b0 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1763,7 +1763,9 @@ def test_output_locking_when_writeback_diff(self, color: bool) -> None: src = (workspace / f"test{tag}.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - with patch("black.Manager", wraps=multiprocessing.Manager) as mgr: + with patch( + "black.concurrency.Manager", wraps=multiprocessing.Manager + ) as mgr: cmd = ["--diff", str(workspace)] if color: cmd.append("--color") @@ -1810,7 +1812,7 @@ def test_filter_cached(self) -> None: str(cached): black.get_cache_info(cached), str(cached_but_changed): (0.0, 0), } - todo, done = black.filter_cached( + todo, done = black.cache.filter_cached( cache, {uncached, cached, cached_but_changed} ) assert todo == {uncached, cached_but_changed} From afed2c01903465f9a486ac481a66aa3413cc1b01 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 5 Aug 2022 14:04:43 -0400 Subject: [PATCH 298/700] Load .gitignore and exclude regex at time of use Loading .gitignore and compiling the exclude regex can take more than 15ms. We shouldn't and don't need to pay this cost if we're simply formatting files given on the command line directly. I would've loved to lazily import pathspec, but the patch won't be clean until the file collection and discovery logic is refactored first. Co-authored-by: Fabio Zadrozny --- src/black/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index afc76e1fa0c..117dc832c98 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -623,12 +623,7 @@ def get_sources( ) -> Set[Path]: """Compute the set of files to be formatted.""" sources: Set[Path] = set() - - if exclude is None: - exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) - gitignore = get_gitignore(ctx.obj["root"]) - else: - gitignore = None + root = ctx.obj["root"] for s in src: if s == "-" and stdin_filename: @@ -663,6 +658,11 @@ def get_sources( sources.add(p) elif p.is_dir(): + if exclude is None: + exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) + gitignore = get_gitignore(root) + else: + gitignore = None sources.update( gen_python_files( p.iterdir(), From c0cc19b5b3371842d696875897bebefebd5e1596 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 13 Aug 2022 13:46:52 -0400 Subject: [PATCH 299/700] Delay worker count determination os.cpu_count() can return None (sounds like a super arcane edge case though) so the type annotation for the `workers` parameter of `black.main` is wrong. This *could* technically cause a runtime TypeError since it'd trip one of mypyc's runtime type checks so we might as well fix it. Reading the documentation (and cross-checking with the source code), you are actually allowed to pass None as `max_workers` to `concurrent.futures.ProcessPoolExecutor`. If it is None, the pool initializer will simply call os.cpu_count() [^1] (defaulting to 1 if it returns None [^2]). It'll even round down the worker count to a level that's safe for Windows. ... so theoretically we don't even need to call os.cpu_count() ourselves, but our Windows limit is 60 (unlike the stdlib's 61) and I'd prefer not accidentally reintroducing a crash on machines with many, many CPU cores. [^1]: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor [^2]: https://github.com/python/cpython/blob/a372a7d65320396d44e8beb976e3a6c382963d4e/Lib/concurrent/futures/process.py#L600 --- src/black/__init__.py | 14 +++----------- src/black/concurrency.py | 11 ++++++----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 117dc832c98..86a0b637442 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,6 +1,5 @@ import io import json -import os import platform import re import sys @@ -28,11 +27,6 @@ Union, ) -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final - import click from click.core import ParameterSource from mypy_extensions import mypyc_attr @@ -92,7 +86,6 @@ from blib2to3.pytree import Leaf, Node COMPILED = Path(__file__).suffix in (".pyd", ".so") -DEFAULT_WORKERS: Final = os.cpu_count() # types FileContent = str @@ -371,9 +364,8 @@ def validate_regex( "-W", "--workers", type=click.IntRange(min=1), - default=DEFAULT_WORKERS, - show_default=True, - help="Number of parallel workers", + default=None, + help="Number of parallel workers [default: number of CPUs in the system]", ) @click.option( "-q", @@ -448,7 +440,7 @@ def main( # noqa: C901 extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], stdin_filename: Optional[str], - workers: int, + workers: Optional[int], src: Tuple[str, ...], config: Optional[str], ) -> None: diff --git a/src/black/concurrency.py b/src/black/concurrency.py index d77ea40bd46..bdc368d5add 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -6,6 +6,7 @@ import asyncio import logging +import os import signal import sys from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor @@ -15,7 +16,7 @@ from mypy_extensions import mypyc_attr -from black import DEFAULT_WORKERS, WriteBack, format_file_in_place +from black import WriteBack, format_file_in_place from black.cache import Cache, filter_cached, read_cache, write_cache from black.mode import Mode from black.output import err @@ -87,13 +88,13 @@ def reformat_many( maybe_install_uvloop() executor: Executor - worker_count = workers if workers is not None else DEFAULT_WORKERS + if workers is None: + workers = os.cpu_count() or 1 if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 - assert worker_count is not None - worker_count = min(worker_count, 60) + workers = min(workers, 60) try: - executor = ProcessPoolExecutor(max_workers=worker_count) + executor = ProcessPoolExecutor(max_workers=workers) except (ImportError, NotImplementedError, OSError): # we arrive here if the underlying system does not support multi-processing # like in AWS Lambda or Termux, in which case we gracefully fallback to From ba618a307a30a119b4fafe526ebf7d5f092ba981 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 30 Aug 2022 19:52:00 -0700 Subject: [PATCH 300/700] Add parens around implicit string concatenations where it increases readability (#3162) Adds parentheses around implicit string concatenations when it's inside a list, set, or tuple. Except when it's only element and there's no trailing comma. Looking at the order of the transformers here, we need to "wrap in parens" before string_split runs. So my solution is to introduce a "collaboration" between StringSplitter and StringParenWrapper where the splitter "skips" the split until the wrapper adds the parens (and then the line after the paren is split by StringSplitter) in another pass. I have also considered an alternative approach, where I tried to add a different "string paren wrapper" class, and it runs before string_split. Then I found out it requires a different do_transform implementation than StringParenWrapper.do_transform, since the later assumes it runs after the delimiter_split transform. So I stopped researching that route. Originally function calls were also included in this change, but given missing commas should usually result in a runtime error and the scary amount of changes this cause on downstream code, they were removed in later revisions. --- CHANGES.md | 2 + src/black/trans.py | 50 +++++++++++- tests/data/preview/comments7.py | 46 +++++++---- tests/data/preview/long_strings.py | 81 ++++++++++++++++++- .../data/preview/long_strings__regression.py | 32 +++++--- tests/test_black.py | 24 +++--- 6 files changed, 191 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 34c54710775..a5ce3b1fbe2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ normalized as expected (#3168) - When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from subscript expressions with more than 1 element (#3209) +- Implicitly concatenated strings inside a list, set, or tuple are now wrapped inside + parentheses (#3162) - Fix a string merging/split issue when a comment is present in the middle of implicitly concatenated strings on its own line (#3227) diff --git a/src/black/trans.py b/src/black/trans.py index 9e0284cefe3..7ecfcef703d 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1043,6 +1043,41 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: max_string_length = self.line_length - offset return max_string_length + @staticmethod + def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]: + """ + Returns: + string_idx such that @LL[string_idx] is equal to our target (i.e. + matched) string, if this line matches the "prefer paren wrap" statement + requirements listed in the 'Requirements' section of the StringParenWrapper + class's docstring. + OR + None, otherwise. + """ + # The line must start with a string. + if LL[0].type != token.STRING: + return None + + matching_nodes = [ + syms.listmaker, + syms.dictsetmaker, + syms.testlist_gexp, + ] + # If the string is an immediate child of a list/set/tuple literal... + if ( + parent_type(LL[0]) in matching_nodes + or parent_type(LL[0].parent) in matching_nodes + ): + # And the string is surrounded by commas (or is the first/last child)... + prev_sibling = LL[0].prev_sibling + next_sibling = LL[0].next_sibling + if (not prev_sibling or prev_sibling.type == token.COMMA) and ( + not next_sibling or next_sibling.type == token.COMMA + ): + return 0 + + return None + def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]: """ @@ -1138,6 +1173,9 @@ class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): def do_splitter_match(self, line: Line) -> TMatchResult: LL = line.leaves + if self._prefer_paren_wrap_match(LL) is not None: + return TErr("Line needs to be wrapped in parens first.") + is_valid_index = is_valid_index_factory(LL) idx = 0 @@ -1583,8 +1621,7 @@ def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]: class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): """ - StringTransformer that splits non-"atom" strings (i.e. strings that do not - exist on lines by themselves). + StringTransformer that wraps strings in parens and then splits at the LPAR. Requirements: All of the requirements listed in BaseStringSplitter's docstring in @@ -1604,6 +1641,11 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): OR * The line is a dictionary key assignment where some valid key is being assigned the value of some string. + OR + * The line starts with an "atom" string that prefers to be wrapped in + parens. It's preferred to be wrapped when it's is an immediate child of + a list/set/tuple literal, AND the string is surrounded by commas (or is + the first/last child). Transformations: The chosen string is wrapped in parentheses and then split at the LPAR. @@ -1628,6 +1670,9 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): changed such that it no longer needs to be given its own line, StringParenWrapper relies on StringParenStripper to clean up the parentheses it created. + + For "atom" strings that prefers to be wrapped in parens, it requires + StringSplitter to hold the split until the string is wrapped in parens. """ def do_splitter_match(self, line: Line) -> TMatchResult: @@ -1644,6 +1689,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult: or self._assert_match(LL) or self._assign_match(LL) or self._dict_match(LL) + or self._prefer_paren_wrap_match(LL) ) if string_idx is not None: diff --git a/tests/data/preview/comments7.py b/tests/data/preview/comments7.py index ca9d7c62b21..ec2dc501d8e 100644 --- a/tests/data/preview/comments7.py +++ b/tests/data/preview/comments7.py @@ -226,39 +226,53 @@ class C: # metadata_version errors. ( {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", + ( + "None is an invalid value for Metadata-Version. Error: This field" + " is required. see" + " https://packaging.python.org/specifications/core-metadata" + ), ), ( {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", + ( + "'-1' is an invalid value for Metadata-Version. Error: Unknown" + " Metadata Version see" + " https://packaging.python.org/specifications/core-metadata" + ), ), # name errors. ( {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", + ( + "'' is an invalid value for Name. Error: This field is required." + " see https://packaging.python.org/specifications/core-metadata" + ), ), ( {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", + ( + "'foo-' is an invalid value for Name. Error: Must start and end" + " with a letter or numeral and contain only ascii numeric and '.'," + " '_' and '-'. see" + " https://packaging.python.org/specifications/core-metadata" + ), ), # version errors. ( {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", + ( + "'' is an invalid value for Version. Error: This field is required." + " see https://packaging.python.org/specifications/core-metadata" + ), ), ( {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", + ( + "'dog' is an invalid value for Version. Error: Must start and end" + " with a letter or numeral and contain only ascii numeric and '.'," + " '_' and '-'. see" + " https://packaging.python.org/specifications/core-metadata" + ), ), ], ) diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 26400eea450..3ad5f355e33 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -18,6 +18,18 @@ D4 = {"A long and ridiculous {}".format(string_key): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", some_func("calling", "some", "stuff"): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format(sooo="soooo", x=2), "A %s %s" % ("formatted", "string"): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." % ("soooo", 2)} +L1 = ["The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a list literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a list literal.", ("parens should be stripped for short string in list")] + +L2 = ["This is a really long string that can't be expected to fit in one line and is the only child of a list literal."] + +S1 = {"The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a set literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a set literal.", ("parens should be stripped for short string in set")} + +S2 = {"This is a really long string that can't be expected to fit in one line and is the only child of a set literal."} + +T1 = ("The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a tuple literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a tuple literal.", ("parens should be stripped for short string in list")) + +T2 = ("This is a really long string that can't be expected to fit in one line and is the only child of a tuple literal.",) + func_with_keywords(my_arg, my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.") bad_split1 = ( @@ -109,7 +121,7 @@ comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. -arg_comment_string = print("Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. +arg_comment_string = print("Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment gets thrown to the top. "Arg #2", "Arg #3", "Arg #4", "Arg #5") pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 @@ -345,6 +357,71 @@ def foo(): % ("soooo", 2), } +L1 = [ + "The is a short string", + ( + "This is a really long string that can't possibly be expected to fit all" + " together on one line. Also it is inside a list literal, so it's expected to" + " be wrapped in parens when spliting to avoid implicit str concatenation." + ), + short_call("arg", {"key": "value"}), + ( + "This is another really really (not really) long string that also can't be" + " expected to fit on one line and is, like the other string, inside a list" + " literal." + ), + "parens should be stripped for short string in list", +] + +L2 = [ + "This is a really long string that can't be expected to fit in one line and is the" + " only child of a list literal." +] + +S1 = { + "The is a short string", + ( + "This is a really long string that can't possibly be expected to fit all" + " together on one line. Also it is inside a set literal, so it's expected to be" + " wrapped in parens when spliting to avoid implicit str concatenation." + ), + short_call("arg", {"key": "value"}), + ( + "This is another really really (not really) long string that also can't be" + " expected to fit on one line and is, like the other string, inside a set" + " literal." + ), + "parens should be stripped for short string in set", +} + +S2 = { + "This is a really long string that can't be expected to fit in one line and is the" + " only child of a set literal." +} + +T1 = ( + "The is a short string", + ( + "This is a really long string that can't possibly be expected to fit all" + " together on one line. Also it is inside a tuple literal, so it's expected to" + " be wrapped in parens when spliting to avoid implicit str concatenation." + ), + short_call("arg", {"key": "value"}), + ( + "This is another really really (not really) long string that also can't be" + " expected to fit on one line and is, like the other string, inside a tuple" + " literal." + ), + "parens should be stripped for short string in list", +) + +T2 = ( + ( + "This is a really long string that can't be expected to fit in one line and is" + " the only child of a tuple literal." + ), +) + func_with_keywords( my_arg, my_kwarg=( @@ -487,7 +564,7 @@ def foo(): arg_comment_string = print( "Long lines with inline comments which are apart of (and not the only member of) an" " argument list should have their comments appended to the reformatted string's" - " enclosing left parentheses.", # This comment stays on the bottom. + " enclosing left parentheses.", # This comment gets thrown to the top. "Arg #2", "Arg #3", "Arg #4", diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 58ccc4ac0b1..634db46a5e0 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -763,20 +763,28 @@ def xxxx_xxx_xx_xxxxxxxxxx_xxxx_xxxxxxxxx(xxxx): some_dictionary = { "xxxxx006": [ - "xxx-xxx" - " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx==" - " xxxxx000 xxxxxxxxxx\n", - "xxx-xxx" - " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx==" - " xxxxx010 xxxxxxxxxx\n", + ( + "xxx-xxx" + " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx==" + " xxxxx000 xxxxxxxxxx\n" + ), + ( + "xxx-xxx" + " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx==" + " xxxxx010 xxxxxxxxxx\n" + ), ], "xxxxx016": [ - "xxx-xxx" - " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx==" - " xxxxx000 xxxxxxxxxx\n", - "xxx-xxx" - " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx==" - " xxxxx010 xxxxxxxxxx\n", + ( + "xxx-xxx" + " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx==" + " xxxxx000 xxxxxxxxxx\n" + ), + ( + "xxx-xxx" + " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx==" + " xxxxx010 xxxxxxxxxx\n" + ), ], } diff --git a/tests/test_black.py b/tests/test_black.py index 5da247b71b0..089e043d639 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -513,15 +513,15 @@ def err(msg: str, **kwargs: Any) -> None: report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_report_quiet(self) -> None: @@ -607,15 +607,15 @@ def err(msg: str, **kwargs: Any) -> None: report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_report_normal(self) -> None: @@ -704,15 +704,15 @@ def err(msg: str, **kwargs: Any) -> None: report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2 files" - " would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_lib2to3_parse(self) -> None: From 2c90480e1a102ab0fac57737d2ba5143d82abed7 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:46:46 -0700 Subject: [PATCH 301/700] Use strict mypy checking (#3222) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .pre-commit-config.yaml | 2 ++ mypy.ini | 45 ++++++++++++++++++++++----------------- scripts/fuzz.py | 5 ++++- src/blackd/middlewares.py | 4 ++-- src/blib2to3/pytree.py | 18 ++++++++-------- tests/optional.py | 2 +- tests/test_blackd.py | 24 +++++++++++++-------- 7 files changed, 59 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87bb6e62987..0be8dc42890 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,6 +49,8 @@ repos: - types-typed-ast >= 1.4.1 - click >= 8.1.0 - platformdirs >= 2.1.0 + - pytest + - hypothesis - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 diff --git a/mypy.ini b/mypy.ini index 244e8ae92f5..4811cc0be76 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,33 +7,40 @@ python_version=3.6 mypy_path=src show_column_numbers=True - -# show error messages from unrelated files -follow_imports=normal - -# suppress errors about unsatisfied imports -ignore_missing_imports=True +show_error_codes=True # be strict -disallow_untyped_calls=True -warn_return_any=True -strict_optional=True -warn_no_return=True -warn_redundant_casts=True -warn_unused_ignores=True -disallow_any_generics=True -no_implicit_optional=True +strict=True + +# except for... +no_implicit_reexport = False # Unreachable blocks have been an issue when compiling mypyc, let's try # to avoid 'em in the first place. warn_unreachable=True -# The following are off by default. Flip them on if you feel -# adventurous. -disallow_untyped_defs=True -check_untyped_defs=True - [mypy-black] # The following is because of `patch_click()`. Remove when # we drop Python 3.6 support. warn_unused_ignores=False + +[mypy-blib2to3.driver.*] +ignore_missing_imports = True + +[mypy-IPython.*] +ignore_missing_imports = True + +[mypy-colorama.*] +ignore_missing_imports = True + +[mypy-pathspec.*] +ignore_missing_imports = True + +[mypy-tokenize_rt.*] +ignore_missing_imports = True + +[mypy-uvloop.*] +ignore_missing_imports = True + +[mypy-_black_version.*] +ignore_missing_imports = True diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 83e02f45152..25362c927d4 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -85,5 +85,8 @@ def test_idempotent_any_syntatically_valid_python( pass else: test = test_idempotent_any_syntatically_valid_python - atheris.Setup(sys.argv, test.hypothesis.fuzz_one_input) + atheris.Setup( + sys.argv, + test.hypothesis.fuzz_one_input, # type: ignore[attr-defined] + ) atheris.Fuzz() diff --git a/src/blackd/middlewares.py b/src/blackd/middlewares.py index 7abde525bfd..e71f5082686 100644 --- a/src/blackd/middlewares.py +++ b/src/blackd/middlewares.py @@ -9,7 +9,7 @@ def cors(allow_headers: Iterable[str]) -> Middleware: - @middleware + @middleware # type: ignore[misc] async def impl(request: Request, handler: Handler) -> StreamResponse: is_options = request.method == "OPTIONS" is_preflight = is_options and "Access-Control-Request-Method" in request.headers @@ -32,4 +32,4 @@ async def impl(request: Request, handler: Handler) -> StreamResponse: return resp - return impl # type: ignore + return impl # type: ignore[no-any-return] diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 10b4690218e..15a1420ef7d 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -10,7 +10,7 @@ There's also a pattern matching implementation here. """ -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, allow-incomplete-defs from typing import ( Any, @@ -291,7 +291,7 @@ def __str__(self) -> Text: """ return "".join(map(str, self.children)) - def _eq(self, other) -> bool: + def _eq(self, other: Base) -> bool: """Compare two nodes for equality.""" return (self.type, self.children) == (other.type, other.children) @@ -326,7 +326,7 @@ def prefix(self) -> Text: return self.children[0].prefix @prefix.setter - def prefix(self, prefix) -> None: + def prefix(self, prefix: Text) -> None: if self.children: self.children[0].prefix = prefix @@ -439,7 +439,7 @@ def __str__(self) -> Text: """ return self._prefix + str(self.value) - def _eq(self, other) -> bool: + def _eq(self, other: "Leaf") -> bool: """Compare two nodes for equality.""" return (self.type, self.value) == (other.type, other.value) @@ -472,7 +472,7 @@ def prefix(self) -> Text: return self._prefix @prefix.setter - def prefix(self, prefix) -> None: + def prefix(self, prefix: Text) -> None: self.changed() self._prefix = prefix @@ -618,7 +618,7 @@ def __init__( self.content = content self.name = name - def match(self, node: NL, results=None): + def match(self, node: NL, results=None) -> bool: """Override match() to insist on a leaf node.""" if not isinstance(node, Leaf): return False @@ -678,7 +678,7 @@ def __init__( if isinstance(item, WildcardPattern): # type: ignore[unreachable] self.wildcards = True # type: ignore[unreachable] self.type = type - self.content = newcontent + self.content = newcontent # TODO: this is unbound when content is None self.name = name def _submatch(self, node, results=None) -> bool: @@ -920,7 +920,7 @@ def _recursive_matches(self, nodes, count) -> Iterator[Tuple[int, _Results]]: class NegatedPattern(BasePattern): - def __init__(self, content: Optional[Any] = None) -> None: + def __init__(self, content: Optional[BasePattern] = None) -> None: """ Initializer. @@ -941,7 +941,7 @@ def match_seq(self, nodes, results=None) -> bool: # We only match an empty sequence of nodes in its entirety return len(nodes) == 0 - def generate_matches(self, nodes) -> Iterator[Tuple[int, _Results]]: + def generate_matches(self, nodes: List[NL]) -> Iterator[Tuple[int, _Results]]: if self.content is None: # Return a match if there is an empty sequence if len(nodes) == 0: diff --git a/tests/optional.py b/tests/optional.py index 853ecaa2a43..8a39cc440a6 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -26,7 +26,7 @@ from pytest import StashKey except ImportError: # pytest < 7 - from _pytest.store import StoreKey as StashKey + from _pytest.store import StoreKey as StashKey # type: ignore[no-redef] log = logging.getLogger(__name__) diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 8e739063f6e..1da4ab702d2 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,6 +1,6 @@ import re import sys -from typing import Any +from typing import TYPE_CHECKING, Any, Callable, TypeVar from unittest.mock import patch import pytest @@ -19,16 +19,22 @@ except ImportError as e: raise RuntimeError("Please install Black with the 'd' extra") from e - try: - from aiohttp.test_utils import unittest_run_loop - except ImportError: - # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and aiohttp 4 - # removed it. To maintain compatibility we can make our own no-op decorator. - def unittest_run_loop(func: Any, *args: Any, **kwargs: Any) -> Any: - return func + if TYPE_CHECKING: + F = TypeVar("F", bound=Callable[..., Any]) + + unittest_run_loop: Callable[[F], F] = lambda x: x + else: + try: + from aiohttp.test_utils import unittest_run_loop + except ImportError: + # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and + # aiohttp 4 removed it. To maintain compatibility we can make our own + # no-op decorator. + def unittest_run_loop(func, *args, **kwargs): + return func @pytest.mark.blackd - class BlackDTestCase(AioHTTPTestCase): + class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] def test_blackd_main(self) -> None: with patch("blackd.web.run_app"): result = CliRunner().invoke(blackd.main, []) From 767604e03f5e454ae5b5c268cd5831c672f46de8 Mon Sep 17 00:00:00 2001 From: Martin de La Gorce Date: Wed, 31 Aug 2022 20:47:42 +0100 Subject: [PATCH 302/700] Use .gitignore files in the initial source directories (#3237) Solves https://github.com/psf/black/issues/2598 where Black wouldn't use .gitignore at folder/.gitignore if you ran `black folder` for example. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 3 +++ src/black/__init__.py | 5 +++++ tests/test_black.py | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index a5ce3b1fbe2..6aa81a8f5c2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,9 @@ - Black now uses the presence of debug f-strings to detect target version. (#3215) - Fix misdetection of project root and verbose logging of sources in cases involving `--stdin-filename` (#3216) +- Immediate `.gitignore` files in source directories given on the command line are now + also respected, previously only `.gitignore` files in the project root and + automatically discovered directories were respected (#3237) ### Documentation diff --git a/src/black/__init__.py b/src/black/__init__.py index 86a0b637442..ded4a736822 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -653,6 +653,11 @@ def get_sources( if exclude is None: exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) gitignore = get_gitignore(root) + p_gitignore = get_gitignore(p) + # No need to use p's gitignore if it is identical to root's gitignore + # (i.e. root and p point to the same directory). + if gitignore != p_gitignore: + gitignore += p_gitignore else: gitignore = None sources.update( diff --git a/tests/test_black.py b/tests/test_black.py index 089e043d639..abd4d00b8e8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1990,6 +1990,13 @@ def test_nested_gitignore(self) -> None: ) assert sorted(expected) == sorted(sources) + def test_nested_gitignore_directly_in_source_directory(self) -> None: + # https://github.com/psf/black/issues/2598 + path = Path(DATA_DIR / "nested_gitignore_tests") + src = Path(path / "root" / "child") + expected = [src / "a.py", src / "c.py"] + assert_collected_sources([src], expected) + def test_invalid_gitignore(self) -> None: path = THIS_DIR / "data" / "invalid_gitignore_tests" empty_config = path / "pyproject.toml" From 7757078ecd84d349bb24ab61e79062ba50162ef9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:46:48 -0400 Subject: [PATCH 303/700] Improve & update release process to reflect recent changes (#3242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Formalise release cadence guidelines - Overhaul release steps to be easier to follow and more thorough - Reorder changelog template to something more sensible - Update release automation docs to reflect recent improvements (notably the addition of in-repo mypyc wheel builds) Co-authored-by: Felix Hildén Co-authored-by: Jelle Zijlstra --- docs/contributing/release_process.md | 237 +++++++++++++++++---------- docs/faq.md | 2 + docs/the_black_code_style/index.md | 2 + 3 files changed, 156 insertions(+), 85 deletions(-) diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 6a4b8680808..be9b08a6c82 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -1,40 +1,85 @@ # Release process -_Black_ has had a lot of work automating its release process. This document sets out to -explain what everything does and how to release _Black_ using said automation. - -## Cutting a Release - -To cut a release, you must be a _Black_ maintainer with `GitHub Release` creation -access. Using this access, the release process is: - -1. Cut a new PR editing `CHANGES.md` and the docs to version the latest changes +_Black_ has had a lot of work done into standardizing and automating its release +process. This document sets out to explain how everything works and how to release +_Black_ using said automation. + +## Release cadence + +**We aim to release whatever is on `main` every 1-2 months.** This ensures merged +improvements and bugfixes are shipped to users reasonably quickly, while not massively +fracturing the user-base with too many versions. This also keeps the workload on +maintainers consistent and predictable. + +If there's not much new on `main` to justify a release, it's acceptable to skip a +month's release. Ideally January releases should not be skipped because as per our +[stability policy](labels/stability-policy), the first release in a new calendar year +may make changes to the _stable_ style. While the policy applies to the first release +(instead of only January releases), confining changes to the stable style to January +will keep things predictable (and nicer) for users. + +Unless there is a serious regression or bug that requires immediate patching, **there +should not be more than one release per month**. While version numbers are cheap, +releases require a maintainer to both commit to do the actual cutting of a release, but +also to be able to deal with the potential fallout post-release. Releasing more +frequently than monthly nets rapidly diminishing returns. + +## Cutting a release + +**You must have `write` permissions for the _Black_ repository to cut a release.** + +The 10,000 foot view of the release process is that you prepare a release PR and then +publish a [GitHub Release]. This triggers [release automation](#release-workflows) that +builds all release artifacts and publishes them to the various platforms we publish to. + +To cut a release: + +1. Determine the release's version number + - **_Black_ follows the [CalVer] versioning standard using the `YY.M.N` format** + - So unless there already has been a release during this month, `N` should be `0` + - Example: the first release in January, 2022 → `22.1.0` +1. File a PR editing `CHANGES.md` and the docs to version the latest changes + 1. Replace the `## Unreleased` header with the version number 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) -3. Open `CHANGES.md` and copy the _raw markdown_ of the latest changes to use in the - description of the GitHub Release. -4. Go and [cut a release](https://github.com/psf/black/releases) using the GitHub UI so - that all workflows noted below are triggered. - 1. The release version and tag should be the [CalVer](https://calver.org) version - _Black_ used for the current release e.g. `21.6` / `21.5b1` - 2. _Black_ uses [setuptools scm](https://pypi.org/project/setuptools-scm/) to pull - the current version for the package builds and release. -5. Once the release is cut, you're basically done. It's a good practice to go and watch - to make sure all the [GitHub Actions](https://github.com/psf/black/actions) pass, - although you should receive an email to your registered GitHub email address should - one fail. - 1. You should see all the release workflows and lint/unittests workflows running on - the new tag in the Actions UI - -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 + 1. (_optional_) Read through and copy-edit the changelog (eg. by moving entries, + fixing typos, or rephrasing entries) + 1. Add a new empty template for the next release above + ([template below](#changelog-template)) + 1. Update references to the latest version in + {doc}`/integrations/source_version_control` and + {doc}`/usage_and_configuration/the_basics` + - Example PR: [GH-3139] +1. Once the release PR is merged, wait until all CI passes + - If CI does not pass, **stop** and investigate the failure(s) as generally we'd want + to fix failing CI before cutting a release +1. [Draft a new GitHub Release][new-release] + 1. Click `Choose a tag` and type in the version number, then select the + `Create new tag: YY.M.N on publish` option that appears + 1. Verify that the new tag targets the `main` branch + 1. You can leave the release title blank, GitHub will default to the tag name + 1. Copy and paste the _raw changelog Markdown_ for the current release into the + description box +1. Publish the GitHub Release, triggering [release automation](#release-workflows) that + will handle the rest +1. At this point, you're basically done. It's good practice to go and [watch and verify + that all the release workflows pass][black-actions], although you will receive a + GitHub notification should something fail. + - If something fails, don't panic. Please go read the respective workflow's logs and + configuration file to reverse-engineer your way to a fix/solution. + +Congratulations! You've successfully cut a new release of _Black_. Go and stand up and +take a break, you deserve it. + +```{important} +Once the release artifacts reach PyPI, you may see new issues being filed indicating +regressions. While regressions are not great, they don't automatically mean a hotfix +release is warranted. Unless the regressions are serious and impact many users, a hotfix +release is probably unnecessary. + +In the end, use your best judgement and ask other maintainers for their thoughts. +``` + +### Changelog template Use the following template for a clean changelog after the release: @@ -45,7 +90,7 @@ Use the following template for a clean changelog after the release: -### Style +### Stable style @@ -53,93 +98,115 @@ Use the following template for a clean changelog after the release: -### _Blackd_ - - - ### Configuration -### Documentation +### Packaging - + -### Integrations +### Parser - + + +### Performance + + ### Output -### Packaging - - +### _Blackd_ -### Parser + - +### Integrations -### Performance + - +### Documentation + ``` ## Release workflows -All _Blacks_'s automation workflows use GitHub Actions. All workflows are therefore -configured using `.yml` files in the `.github/workflows` directory of the _Black_ +All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore +configured using YAML files in the `.github/workflows` directory of the _Black_ repository. +They are triggered by the publication of a [GitHub Release]. + Below are descriptions of our release workflows. -### Docker +### Publish to PyPI + +This is our main workflow. It builds an [sdist] and [wheels] to upload to PyPI where the +vast majority of users will download Black from. It's divided into three job groups: + +#### sdist + pure wheel -This workflow uses the QEMU powered `buildx` feature of docker to upload a `arm64` and -`amd64`/`x86_64` build of the official _Black_ docker image™. +This single job builds the sdist and pure Python wheel (i.e., a wheel that only contains +Python code) using [build] and then uploads them to PyPI using [twine]. These artifacts +are general-purpose and can be used on basically any platform supported by Python. -- Currently this workflow uses an API Token associated with @cooperlees account +#### mypyc wheels (…) -### pypi_upload +We use [mypyc] to compile _Black_ into a CPython C extension for significantly improved +performance. Wheels built with mypyc are platform and Python version specific. +[Supported platforms are documented in the FAQ](labels/mypyc-support). -This workflow builds a Python -[sdist](https://docs.python.org/3/distutils/sourcedist.html) and -[wheel](https://pythonwheels.com) using the latest -[setuptools](https://pypi.org/project/setuptools/) and -[wheel](https://pypi.org/project/wheel/) modules. +These matrix jobs use [cibuildwheel] which handles the complicated task of building C +extensions for many environments for us. Since building these wheels is slow, there are +multiple mypyc wheels jobs (hence the term "matrix") that build for a specific platform +(as noted in the job name in parentheses). -It will then use [twine](https://pypi.org/project/twine/) to upload both release formats -to PyPI for general downloading of the _Black_ Python package. This is where -[pip](https://pypi.org/project/pip/) looks by default. +Like the previous job group, the built wheels are uploaded to PyPI using [twine]. -- Currently this workflow uses an API token associated with @ambv's PyPI account +#### Update stable branch -### Upload self-contained binaries +So this job doesn't _really_ belong here, but updating the `stable` branch after the +other PyPI jobs pass (they must pass for this job to start) makes the most sense. This +saves us from remembering to update the branch sometime after cutting the release. -This workflow builds self-contained binaries for multiple platforms. This allows people -to download the executable for their platform and run _Black_ without a -[Python Runtime](https://wiki.python.org/moin/PythonImplementations) installed. +- _Currently this workflow uses an API token associated with @ambv's PyPI account_ -The created binaries are attached/stored on the associated -[GitHub Release](https://github.com/psf/black/releases) for download over _IPv4 only_ -(GitHub still does not have IPv6 access 😢). +### Publish executables -## Moving the `stable` tag +This workflow builds native executables for multiple platforms using [PyInstaller]. This +allows people to download the executable for their platform and run _Black_ without a +[Python runtime](https://wiki.python.org/moin/PythonImplementations) installed. -_Black_ provides a stable tag for people who want to move along as _Black_ developers -deem the newest version reliable. Here the _Black_ developers will move once the release -has been problem free for at least ~24 hours from release. Given the large _Black_ -userbase we hear about bad bugs quickly. We do strive to continually improve our CI too. +The created binaries are stored on the associated GitHub Release for download over _IPv4 +only_ (GitHub still does not have IPv6 access 😢). -### Tag moving process +### docker -#### stable +This workflow uses the QEMU powered `buildx` feature of Docker to upload an `arm64` and +`amd64`/`x86_64` build of the official _Black_ Docker image™. -From a rebased `main` checkout: +- _Currently this workflow uses an API Token associated with @cooperlees account_ + +```{note} +This also runs on each push to `main`. +``` -1. `git tag -f stable VERSION_TAG` - 1. e.g. `git tag -f stable 21.5b1` -1. `git push --tags -f` +[black-actions]: https://github.com/psf/black/actions +[build]: https://pypa-build.readthedocs.io/ +[calver]: https://calver.org +[cibuildwheel]: https://cibuildwheel.readthedocs.io/ +[gh-3139]: https://github.com/psf/black/pull/3139 +[github actions]: https://github.com/features/actions +[github release]: https://github.com/psf/black/releases +[new-release]: https://github.com/psf/black/releases/new +[mypyc]: https://mypyc.readthedocs.io/ +[mypyc-platform-support]: + /faq.html#what-is-compiled-yes-no-all-about-in-the-version-output +[pyinstaller]: https://www.pyinstaller.org/ +[sdist]: + https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist +[twine]: https://github.com/features/actions +[wheels]: https://packaging.python.org/en/latest/glossary/#term-Wheel diff --git a/docs/faq.md b/docs/faq.md index b2fe42de282..aeb9634789f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -114,6 +114,8 @@ errors is not a goal. It can format all code accepted by CPython (if you find an where that doesn't hold, please report a bug!), but it may also format some code that CPython doesn't accept. +(labels/mypyc-support)= + ## What is `compiled: yes/no` all about in the version output? While _Black_ is indeed a pure Python project, we use [mypyc] to compile _Black_ into a diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md index c7f29af6c73..e5967be2db4 100644 --- a/docs/the_black_code_style/index.md +++ b/docs/the_black_code_style/index.md @@ -19,6 +19,8 @@ style aspects and details might change according to the stability policy present below. Ongoing style considerations are tracked on GitHub with the [design](https://github.com/psf/black/labels/T%3A%20design) issue label. +(labels/stability-policy)= + ## Stability Policy The following policy applies for the _Black_ code style, in non pre-release versions of From 0019261abcf6d9e564ba32d3cc15534b9026f29e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:56:47 -0400 Subject: [PATCH 304/700] Update stable branch after publishing to PyPI (#3223) We've decided to a) convert stable back into a branch and b) to update it immediately as part of the release process. We may as well automate it. And about going back to a branch ... Git tags are not the right tool, at all[^1]. They come with the expectation that they will never change. Things will not work as expected if they do change, doubly so if they change regularly. Once you pull stable from the remote and it's copied in your local repository, no matter how many times you run git pull you'll never see it get updated automatically. Your only recourse is to delete the tag via `git tag -d stable` before pulling. This gets annoying really quickly since stable is supposed to be the solution for folks "who want to move along as Black developers deem the newest version reliable."[^2] See this comment for how this impacts users using our Vim plugin[^3]. It also affects us developers[^4]. If you have stable locally, once we cut a new release and update the stable tag, a simple `git pull` / `git fetch` will not pull down the updated stable tag. Unless you remember to delete stable before pulling, stable will become stale and useless. You can argue this is a good thing ("people should explicitly opt into updating stable"), but IMO it does not match user expectations nor developer expectations[^5]. Especially since not all our integrations that use stable are bound by this security measure, for example our GitHub Action (since it does a clean fetch of the repository every time it's used). I believe consistency would be good here. Finally, ever since we switched to a tag, we've been facing issues with ReadTheDocs not picking up updates to stable unless we force a rebuild. The initial rebuild on the stable update just pulls the commit the tag previously pointed to. I'm not sure if switching back to a branch will fix this, but I'd wager it will. [^1]: https://git-scm.com/docs/git-tag#_on_re_tagging [^2]: https://black.readthedocs.io/en/stable/contributing/release_process.html#moving-the-stable-tag [^3]: https://github.com/psf/black/issues/2503#issuecomment-1196357379 [^4]: In fairness, most folks working on Black probably don't use the `stable` ref anyway, especially us maintainers who'd know what is the latest version by heart, but it'd still be nice to make it usable for local dev though. [^5]: Also what benefit does a `stable` ref have over explicit version tags like `22.6.0`? If you're going to opt into some odd pin mechanism, might as well use explicit version tags for clarity and consistency. --- .github/workflows/pypi_upload.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 31a83266345..d52f41a4939 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -74,3 +74,22 @@ jobs: env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: pipx run twine upload --verbose -u '__token__' wheelhouse/*.whl + + update-stable-branch: + name: Update stable branch + needs: [main, mypyc] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout stable branch + uses: actions/checkout@v3 + with: + ref: stable + fetch-depth: 0 + + - name: Update stable branch to release tag & push + run: | + git reset --hard ${{ github.event.release.tag_name }} + git push From 2018e667a6a36ee3fbfa8041cd36512f92f60d49 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:39:54 -0400 Subject: [PATCH 305/700] Prepare docs for release 22.8.0 (#3248) --- CHANGES.md | 85 +++++++++++++-------- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6aa81a8f5c2..7c7be98f716 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,20 +6,68 @@ -### Style +### Stable style +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + +## 22.8.0 + +### Highlights + +- Python 3.11 is now supported, except for _blackd_ as aiohttp does not support 3.11 as + of publishing (#3234) +- This is the last release that supports running _Black_ on Python 3.6 (formatting 3.6 + code will continue to be supported until further notice) +- Reword the stability policy to say that we may, in rare cases, make changes that + affect code that was not previously formatted by _Black_ (#3155) + +### Stable style + - Fix an infinite loop when using `# fmt: on/off` in the middle of an expression or code block (#3158) -- Fix incorrect handling of `# fmt: skip` on colon `:` lines. (#3148) +- Fix incorrect handling of `# fmt: skip` on colon (`:`) lines (#3148) - Comments are no longer deleted when a line had spaces removed around power operators (#2874) ### Preview style - - - Single-character closing docstring quotes are no longer moved to their own line as this is invalid. This was a bug introduced in version 22.6.0. (#3166) - `--skip-string-normalization` / `-S` now prevents docstring prefixes from being @@ -33,15 +81,11 @@ ### _Blackd_ - - -- `blackd` now supports preview style via `X-Preview` header (#3217) +- `blackd` now supports enabling the preview style via the `X-Preview` header (#3217) ### Configuration - - -- Black now uses the presence of debug f-strings to detect target version. (#3215) +- Black now uses the presence of debug f-strings to detect target version (#3215) - Fix misdetection of project root and verbose logging of sources in cases involving `--stdin-filename` (#3216) - Immediate `.gitignore` files in source directories given on the command line are now @@ -50,48 +94,29 @@ ### Documentation - - -- Reword the stability policy to say that we may, in rare cases, make changes that - affect code that was not previously formatted by _Black_ (#3155) - Recommend using BlackConnect in IntelliJ IDEs (#3150) ### Integrations - - - Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194) - Docker: changed to a /opt/venv installation + added to PATH to be available to non-root users (#3202) ### Output - - - Change from deprecated `asyncio.get_event_loop()` to create our event loop which removes DeprecationWarning (#3164) -- Remove logging from internal `blib2to3` library since it regularily emits error logs +- Remove logging from internal `blib2to3` library since it regularly emits error logs about failed caching that can and should be ignored (#3193) -### Packaging - - - -- Python 3.11 is now supported, except for `blackd` (#3234) - ### Parser - - - Type comments are now included in the AST equivalence check consistently so accidental deletion raises an error. Though type comments can't be tracked when running on PyPy 3.7 due to standard library limitations. (#2874) ### Performance - - - Reduce Black's startup time when formatting a single file by 15-30% (#3211) ## 22.6.0 diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index e897cf669fc..31d0df27273 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: 22.6.0 + rev: 22.8.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 4c358742674..2dc2a14f91a 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 22.6.0 +black, version 22.8.0 ``` An option to require a specific version to be running is also provided. From 095fe0d649541636d7011e779214a146b4f32895 Mon Sep 17 00:00:00 2001 From: James Salvatore Date: Wed, 31 Aug 2022 23:25:13 -0500 Subject: [PATCH 306/700] docs: adds ExitStack alternative to future_style.md (#3247) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- docs/conf.py | 2 +- docs/the_black_code_style/future_style.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 8da9c39ac41..7fc4f8f589e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ def make_pypi_svg(version: str) -> None: # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "3.0" +needs_sphinx = "4.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index fab4bca120e..a17d9a10673 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -34,6 +34,19 @@ 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. +An alternative to consider if the backslashes in the above formatting are undesirable is +to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the +following way: + +```python +with contextlib.ExitStack() as exit_stack: + cm1 = exit_stack.enter_context(make_context_manager(1)) + cm2 = exit_stack.enter_context(make_context_manager(2)) + cm3 = exit_stack.enter_context(make_context_manager(3)) + cm4 = exit_stack.enter_context(make_context_manager(4)) + ... +``` + ## Preview style Experimental, potentially disruptive style changes are gathered under the `--preview` From 92c93a278036870a76740d5b0b8f06504925e7dc Mon Sep 17 00:00:00 2001 From: PeterGrossmann Date: Thu, 1 Sep 2022 18:39:47 +0200 Subject: [PATCH 307/700] Add preview flag to Vim plugin (#3246) This allows the configuration of the --preview flag in the Vim plugin. --- CHANGES.md | 1 + autoload/black.vim | 2 ++ docs/integrations/editors.md | 1 + plugin/black.vim | 3 +++ 4 files changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7c7be98f716..25c3d4889a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -101,6 +101,7 @@ - Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194) - Docker: changed to a /opt/venv installation + added to PATH to be available to non-root users (#3202) +- Vim plugin: add flag (`g:black_preview`) to enable/disable the preview style (#3246) ### Output diff --git a/autoload/black.vim b/autoload/black.vim index ed657be7bd3..e87a1e4edfa 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -30,6 +30,7 @@ FLAGS = [ Flag(name="skip_string_normalization", cast=strtobool), Flag(name="quiet", cast=strtobool), Flag(name="skip_magic_trailing_comma", cast=strtobool), + Flag(name="preview", cast=strtobool), ] @@ -145,6 +146,7 @@ def Black(**kwargs): string_normalization=not configs["skip_string_normalization"], is_pyi=vim.current.buffer.name.endswith('.pyi'), magic_trailing_comma=not configs["skip_magic_trailing_comma"], + preview=configs["preview"], **black_kwargs, ) quiet = configs["quiet"] diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 07bf672f4fd..318e0e295d0 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -113,6 +113,7 @@ Configuration: - `g:black_skip_string_normalization` (defaults to `0`) - `g:black_virtualenv` (defaults to `~/.vim/black` or `~/.local/share/nvim/black`) - `g:black_quiet` (defaults to `0`) +- `g:black_preview` (defaults to `0`) To install with [vim-plug](https://github.com/junegunn/vim-plug): diff --git a/plugin/black.vim b/plugin/black.vim index 3fc11fe9e8d..fb70424b0ef 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -63,6 +63,9 @@ endif if !exists("g:black_target_version") let g:black_target_version = "" endif +if !exists("g:black_preview") + let g:black_preview = 0 +endif function BlackComplete(ArgLead, CmdLine, CursorPos) return [ From 062e644aae4299a320aeac59085df4c020ba6c81 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:27:05 -0400 Subject: [PATCH 308/700] Mitigate deprecation of aiohttp's `@middleware` decorator (#3259) This is deprecated since aiohttp 4.0. If it doesn't exist just define a no-op decorator that does nothing (after the other aiohttp imports though!). By doing this, it's safe to ignore the DeprecationWarning without needing to require the latest aiohttp once they remove `@middleware`. --- pyproject.toml | 3 +++ src/blackd/middlewares.py | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 813e86b2e93..849891f8798 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,9 @@ filterwarnings = [ # this is mitigated by a try/catch in https://github.com/psf/black/pull/2974/ # this ignore can be removed when support for aiohttp 3.7 is dropped. '''ignore:Decorator `@unittest_run_loop` is no longer needed in aiohttp 3\.8\+:DeprecationWarning''', + # this is mitigated by a try/catch in https://github.com/psf/black/pull/3198/ + # this ignore can be removed when support for aiohttp 3.x is dropped. + '''ignore:Middleware decorator is deprecated since 4\.0 and its behaviour is default, you can simply remove this decorator:DeprecationWarning''', # this is mitigated by https://github.com/python/cpython/issues/79071 in python 3.8+ # this ignore can be removed when support for 3.7 is dropped. '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', diff --git a/src/blackd/middlewares.py b/src/blackd/middlewares.py index e71f5082686..370e0ae222e 100644 --- a/src/blackd/middlewares.py +++ b/src/blackd/middlewares.py @@ -1,15 +1,25 @@ -from typing import Awaitable, Callable, Iterable +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, TypeVar -from aiohttp.web_middlewares import middleware from aiohttp.web_request import Request from aiohttp.web_response import StreamResponse +if TYPE_CHECKING: + F = TypeVar("F", bound=Callable[..., Any]) + middleware: Callable[[F], F] +else: + try: + from aiohttp.web_middlewares import middleware + except ImportError: + # @middleware is deprecated and its behaviour is the default since aiohttp 4.0 + # so if it doesn't exist anymore, define a no-op for forward compatibility. + middleware = lambda x: x # noqa: E731 + Handler = Callable[[Request], Awaitable[StreamResponse]] Middleware = Callable[[Request, Handler], Awaitable[StreamResponse]] def cors(allow_headers: Iterable[str]) -> Middleware: - @middleware # type: ignore[misc] + @middleware async def impl(request: Request, handler: Handler) -> StreamResponse: is_options = request.method == "OPTIONS" is_preflight = is_options and "Access-Control-Request-Method" in request.headers @@ -32,4 +42,4 @@ async def impl(request: Request, handler: Handler) -> StreamResponse: return resp - return impl # type: ignore[no-any-return] + return impl From 383b228a1690d9c15ce97bd2e01874596fbf1288 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 6 Sep 2022 08:27:39 +1000 Subject: [PATCH 309/700] Move 3.11 tests to install aiohttp without C extensions (#3258) * Move 311 tests to install aiohttp without C extensions - Configure tox to install aiohttp without extensions - i.e. use `AIOHTTP_NO_EXTENSIONS=1` for pip install - This allows us to reenable blackd tests that use aiohttp testing helpers etc. - Had to ignore `cgi` module deprecation warning - Filed issue for aiohttp to fix: https://github.com/aio-libs/aiohttp/issues/6905 Test: - `/tmp/tb/bin/tox -e 311` * Fix formatting + linting * Add latest aiohttp for loop fix + Try to exempt deprecation warning but failed - will ask for help * Remove unnecessary warning ignore Co-authored-by: Cooper Ry Lees Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/workflows/test-311.yml | 2 +- pyproject.toml | 3 + tests/test_blackd.py | 389 ++++++++++++++++----------------- tox.ini | 8 +- 4 files changed, 203 insertions(+), 199 deletions(-) diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml index e23a67e89eb..c2da2465ad5 100644 --- a/.github/workflows/test-311.yml +++ b/.github/workflows/test-311.yml @@ -1,4 +1,4 @@ -name: Partially test 3.11 dev +name: Test 3.11 without aiohttp extensions on: push: diff --git a/pyproject.toml b/pyproject.toml index 849891f8798..566462c9b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,4 +111,7 @@ filterwarnings = [ # this is mitigated by https://github.com/python/cpython/issues/79071 in python 3.8+ # this ignore can be removed when support for 3.7 is dropped. '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', + # aiohttp is using deprecated cgi modules - Safe to remove when fixed: + # https://github.com/aio-libs/aiohttp/issues/6905 + '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', ] diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 1da4ab702d2..511bd86441d 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,5 +1,4 @@ import re -import sys from typing import TYPE_CHECKING, Any, Callable, TypeVar from unittest.mock import patch @@ -8,207 +7,205 @@ from tests.util import DETERMINISTIC_HEADER, read_data -LESS_THAN_311 = sys.version_info < (3, 11) +try: + from aiohttp import web + from aiohttp.test_utils import AioHTTPTestCase -if LESS_THAN_311: # noqa: C901 - try: - from aiohttp import web - from aiohttp.test_utils import AioHTTPTestCase - - import blackd - except ImportError as e: - raise RuntimeError("Please install Black with the 'd' extra") from e - - if TYPE_CHECKING: - F = TypeVar("F", bound=Callable[..., Any]) - - unittest_run_loop: Callable[[F], F] = lambda x: x - else: - try: - from aiohttp.test_utils import unittest_run_loop - except ImportError: - # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and - # aiohttp 4 removed it. To maintain compatibility we can make our own - # no-op decorator. - def unittest_run_loop(func, *args, **kwargs): - return func - - @pytest.mark.blackd - class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] - def test_blackd_main(self) -> None: - with patch("blackd.web.run_app"): - result = CliRunner().invoke(blackd.main, []) - if result.exception is not None: - raise result.exception - self.assertEqual(result.exit_code, 0) - - async def get_application(self) -> web.Application: - return blackd.make_app() - - @unittest_run_loop - async def test_blackd_request_needs_formatting(self) -> None: - response = await self.client.post("/", data=b"print('hello world')") - self.assertEqual(response.status, 200) - self.assertEqual(response.charset, "utf8") - self.assertEqual(await response.read(), b'print("hello world")\n') - - @unittest_run_loop - async def test_blackd_request_no_change(self) -> None: - response = await self.client.post("/", data=b'print("hello world")\n') - self.assertEqual(response.status, 204) - self.assertEqual(await response.read(), b"") - - @unittest_run_loop - async def test_blackd_request_syntax_error(self) -> None: - response = await self.client.post("/", data=b"what even ( is") - self.assertEqual(response.status, 400) - content = await response.text() - self.assertTrue( - content.startswith("Cannot parse"), - msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", - ) + import blackd +except ImportError as e: + raise RuntimeError("Please install Black with the 'd' extra") from e - @unittest_run_loop - async def test_blackd_unsupported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} - ) - self.assertEqual(response.status, 501) +if TYPE_CHECKING: + F = TypeVar("F", bound=Callable[..., Any]) - @unittest_run_loop - async def test_blackd_supported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_python_variant(self) -> None: - async def check(header_value: str, expected_status: int = 400) -> None: - response = await self.client.post( - "/", - data=b"what", - headers={blackd.PYTHON_VARIANT_HEADER: header_value}, - ) - self.assertEqual(response.status, expected_status) - - await check("lol") - await check("ruby3.5") - await check("pyi3.6") - await check("py1.5") - await check("2") - await check("2.7") - await check("py2.7") - await check("2.8") - await check("py2.8") - await check("3.0") - await check("pypy3.0") - await check("jython3.4") - - @unittest_run_loop - async def test_blackd_pyi(self) -> None: - source, expected = read_data("miscellaneous", "stub.pyi") - response = await self.client.post( - "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} - ) - self.assertEqual(response.status, 200) - self.assertEqual(await response.text(), expected) - - @unittest_run_loop - async def test_blackd_diff(self) -> None: - diff_header = re.compile( - r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" - ) - - source, _ = read_data("miscellaneous", "blackd_diff") - expected, _ = read_data("miscellaneous", "blackd_diff.diff") - - response = await self.client.post( - "/", data=source, headers={blackd.DIFF_HEADER: "true"} - ) - self.assertEqual(response.status, 200) - - actual = await response.text() - actual = diff_header.sub(DETERMINISTIC_HEADER, actual) - self.assertEqual(actual, expected) - - @unittest_run_loop - async def test_blackd_python_variant(self) -> None: - code = ( - "def f(\n" - " and_has_a_bunch_of,\n" - " very_long_arguments_too,\n" - " and_lots_of_them_as_well_lol,\n" - " **and_very_long_keyword_arguments\n" - "):\n" - " pass\n" - ) - - async def check(header_value: str, expected_status: int) -> None: - response = await self.client.post( - "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} - ) - self.assertEqual( - response.status, expected_status, msg=await response.text() - ) - - await check("3.6", 200) - await check("py3.6", 200) - await check("3.6,3.7", 200) - await check("3.6,py3.7", 200) - await check("py36,py37", 200) - await check("36", 200) - await check("3.6.4", 200) - await check("3.4", 204) - await check("py3.4", 204) - await check("py34,py36", 204) - await check("34", 204) - - @unittest_run_loop - async def test_blackd_line_length(self) -> None: - response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_line_length(self) -> None: + unittest_run_loop: Callable[[F], F] = lambda x: x +else: + try: + from aiohttp.test_utils import unittest_run_loop + except ImportError: + # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and + # aiohttp 4 removed it. To maintain compatibility we can make our own + # no-op decorator. + def unittest_run_loop(func, *args, **kwargs): + return func + + +@pytest.mark.blackd +class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] + def test_blackd_main(self) -> None: + with patch("blackd.web.run_app"): + result = CliRunner().invoke(blackd.main, []) + if result.exception is not None: + raise result.exception + self.assertEqual(result.exit_code, 0) + + async def get_application(self) -> web.Application: + return blackd.make_app() + + @unittest_run_loop + async def test_blackd_request_needs_formatting(self) -> None: + response = await self.client.post("/", data=b"print('hello world')") + self.assertEqual(response.status, 200) + self.assertEqual(response.charset, "utf8") + self.assertEqual(await response.read(), b'print("hello world")\n') + + @unittest_run_loop + async def test_blackd_request_no_change(self) -> None: + response = await self.client.post("/", data=b'print("hello world")\n') + self.assertEqual(response.status, 204) + self.assertEqual(await response.read(), b"") + + @unittest_run_loop + async def test_blackd_request_syntax_error(self) -> None: + response = await self.client.post("/", data=b"what even ( is") + self.assertEqual(response.status, 400) + content = await response.text() + self.assertTrue( + content.startswith("Cannot parse"), + msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", + ) + + @unittest_run_loop + async def test_blackd_unsupported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} + ) + self.assertEqual(response.status, 501) + + @unittest_run_loop + async def test_blackd_supported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} + ) + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_python_variant(self) -> None: + async def check(header_value: str, expected_status: int = 400) -> None: response = await self.client.post( "/", - data=b'print("hello")\n', - headers={blackd.LINE_LENGTH_HEADER: "NaN"}, + data=b"what", + headers={blackd.PYTHON_VARIANT_HEADER: header_value}, ) - self.assertEqual(response.status, 400) - - @unittest_run_loop - async def test_blackd_preview(self) -> None: + self.assertEqual(response.status, expected_status) + + await check("lol") + await check("ruby3.5") + await check("pyi3.6") + await check("py1.5") + await check("2") + await check("2.7") + await check("py2.7") + await check("2.8") + await check("py2.8") + await check("3.0") + await check("pypy3.0") + await check("jython3.4") + + @unittest_run_loop + async def test_blackd_pyi(self) -> None: + source, expected = read_data("miscellaneous", "stub.pyi") + response = await self.client.post( + "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} + ) + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), expected) + + @unittest_run_loop + async def test_blackd_diff(self) -> None: + diff_header = re.compile( + r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + + source, _ = read_data("miscellaneous", "blackd_diff") + expected, _ = read_data("miscellaneous", "blackd_diff.diff") + + response = await self.client.post( + "/", data=source, headers={blackd.DIFF_HEADER: "true"} + ) + self.assertEqual(response.status, 200) + + actual = await response.text() + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + + @unittest_run_loop + async def test_blackd_python_variant(self) -> None: + code = ( + "def f(\n" + " and_has_a_bunch_of,\n" + " very_long_arguments_too,\n" + " and_lots_of_them_as_well_lol,\n" + " **and_very_long_keyword_arguments\n" + "):\n" + " pass\n" + ) + + async def check(header_value: str, expected_status: int) -> None: response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} ) - self.assertEqual(response.status, 204) - - @unittest_run_loop - async def test_blackd_response_black_version_header(self) -> None: - response = await self.client.post("/") - self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) - - @unittest_run_loop - async def test_cors_preflight(self) -> None: - response = await self.client.options( - "/", - headers={ - "Access-Control-Request-Method": "POST", - "Origin": "*", - "Access-Control-Request-Headers": "Content-Type", - }, + self.assertEqual( + response.status, expected_status, msg=await response.text() ) - self.assertEqual(response.status, 200) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) - - @unittest_run_loop - async def test_cors_headers_present(self) -> None: - response = await self.client.post("/", headers={"Origin": "*"}) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) + + await check("3.6", 200) + await check("py3.6", 200) + await check("3.6,3.7", 200) + await check("3.6,py3.7", 200) + await check("py36,py37", 200) + await check("36", 200) + await check("3.6.4", 200) + await check("3.4", 204) + await check("py3.4", 204) + await check("py34,py36", 204) + await check("34", 204) + + @unittest_run_loop + async def test_blackd_line_length(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} + ) + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_line_length(self) -> None: + response = await self.client.post( + "/", + data=b'print("hello")\n', + headers={blackd.LINE_LENGTH_HEADER: "NaN"}, + ) + self.assertEqual(response.status, 400) + + @unittest_run_loop + async def test_blackd_preview(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + ) + self.assertEqual(response.status, 204) + + @unittest_run_loop + async def test_blackd_response_black_version_header(self) -> None: + response = await self.client.post("/") + self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) + + @unittest_run_loop + async def test_cors_preflight(self) -> None: + response = await self.client.options( + "/", + headers={ + "Access-Control-Request-Method": "POST", + "Origin": "*", + "Access-Control-Request-Headers": "Content-Type", + }, + ) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) + + @unittest_run_loop + async def test_cors_headers_present(self) -> None: + response = await self.client.post("/", headers={"Origin": "*"}) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) diff --git a/tox.ini b/tox.ini index 5f3874c23b4..098f06c9828 100644 --- a/tox.ini +++ b/tox.ini @@ -51,16 +51,20 @@ commands = coverage report [testenv:{,ci-}311] -setenv = PYTHONPATH = {toxinidir}/src +setenv = + PYTHONPATH = {toxinidir}/src + AIOHTTP_NO_EXTENSIONS = 1 skip_install = True recreate = True deps = +; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11 + git+https://github.com/aio-libs/aiohttp -r{toxinidir}/test_requirements.txt ; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 ; this seems to cause tox to wait forever ; remove this when pypy releases the bugfix commands = - pip install -e . + pip install -e .[d] coverage erase pytest tests \ --run-optional no_jupyter \ From 72a25591b04b40d1c9b67844120457297c93ecb8 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Wed, 14 Sep 2022 05:06:54 +0200 Subject: [PATCH 310/700] [FIX] migrate-black.py: don't fail on binary files (#3266) --- scripts/migrate-black.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/migrate-black.py b/scripts/migrate-black.py index 63cc5096a93..ff52939460c 100755 --- a/scripts/migrate-black.py +++ b/scripts/migrate-black.py @@ -49,6 +49,7 @@ def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> in [ "git", "diff", + "--binary", "--find-copies", "%s-black..%s-black" % (last_commit, commit), ], From e2adcd7de10eb570987bb894d95f2ff8c8693b9f Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 13 Sep 2022 20:23:51 -0700 Subject: [PATCH 311/700] Fix a crash on dicts with paren-wrapped long string keys (#3262) Fix a crash when formatting some dicts with parenthesis-wrapped long string keys. When LL[0] is an atom string, we need to check the atom node's siblings instead of LL[0] itself, e.g.: dictsetmaker atom STRING '"This is a really long string that can\'t be expected to fit in one line and is used as a nested dict\'s key"' /atom COLON ':' atom LSQB ' ' '[' listmaker STRING '"value"' COMMA ',' STRING ' ' '"value"' /listmaker RSQB ']' /atom COMMA ',' /dictsetmaker --- CHANGES.md | 3 +++ src/black/trans.py | 10 ++++++++++ tests/data/preview/long_strings.py | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 25c3d4889a0..147100c3012 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Fix a crash when formatting some dicts with parenthesis-wrapped long string keys + (#3262) + ### Configuration diff --git a/src/black/trans.py b/src/black/trans.py index 7ecfcef703d..74b932bb422 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1071,6 +1071,16 @@ def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]: # And the string is surrounded by commas (or is the first/last child)... prev_sibling = LL[0].prev_sibling next_sibling = LL[0].next_sibling + if ( + not prev_sibling + and not next_sibling + and parent_type(LL[0]) == syms.atom + ): + # If it's an atom string, we need to check the parent atom's siblings. + parent = LL[0].parent + assert parent is not None # For type checkers. + prev_sibling = parent.prev_sibling + next_sibling = parent.next_sibling if (not prev_sibling or prev_sibling.type == token.COMMA) and ( not next_sibling or next_sibling.type == token.COMMA ): diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 3ad5f355e33..6db3cfed9a9 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -18,6 +18,14 @@ D4 = {"A long and ridiculous {}".format(string_key): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", some_func("calling", "some", "stuff"): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format(sooo="soooo", x=2), "A %s %s" % ("formatted", "string"): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." % ("soooo", 2)} +D5 = { # Test for https://github.com/psf/black/issues/3261 + ("This is a really long string that can't be expected to fit in one line and is used as a nested dict's key"): {"inner": "value"}, +} + +D6 = { # Test for https://github.com/psf/black/issues/3261 + ("This is a really long string that can't be expected to fit in one line and is used as a dict's key"): ["value1", "value2"], +} + L1 = ["The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a list literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a list literal.", ("parens should be stripped for short string in list")] L2 = ["This is a really long string that can't be expected to fit in one line and is the only child of a list literal."] @@ -357,6 +365,19 @@ def foo(): % ("soooo", 2), } +D5 = { # Test for https://github.com/psf/black/issues/3261 + "This is a really long string that can't be expected to fit in one line and is used as a nested dict's key": { + "inner": "value" + }, +} + +D6 = { # Test for https://github.com/psf/black/issues/3261 + "This is a really long string that can't be expected to fit in one line and is used as a dict's key": [ + "value1", + "value2", + ], +} + L1 = [ "The is a short string", ( From 04bce6ad2ecf38656149fd261f01f84699cf0b6a Mon Sep 17 00:00:00 2001 From: Tom Fryers <61272761+TomFryers@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:31:26 +0100 Subject: [PATCH 312/700] Improve order of paragraphs on line splitting (#3270) These two paragraphs were tucked away at the end of the section, after the diversion on backslashes. I nearly missed the first paragraph and opened a nonsense issue, and I think the second belongs higher up with it too. --- docs/the_black_code_style/current_style.md | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 5085b0017d9..3db49e2ba01 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -85,6 +85,19 @@ def very_important_function( ... ``` +If a data structure literal (tuple, list, set, dict) or a line of "from" imports cannot +fit in the allotted length, it's always split into one element per line. This minimizes +diffs as well as enables readers of code to find which commit introduced a particular +entry. This also makes _Black_ compatible with +[isort](../guides/using_black_with_other_tools.md#isort) with the ready-made `black` +profile or manual configuration. + +You might have noticed that closing brackets are always dedented and that a trailing +comma is always added. Such formatting produces smaller diffs; when you add or remove an +element, it's always just one line. Also, having the closing bracket dedented provides a +clear delimiter between two distinct sections of the code that otherwise share the same +indentation level (like the arguments list and the docstring in the example above). + (labels/why-no-backslashes)= _Black_ prefers parentheses over backslashes, and will remove backslashes if found. @@ -127,19 +140,6 @@ If you're reaching for backslashes, that's a clear signal that you can do better slightly refactor your code. I hope some of the examples above show you that there are many ways in which you can do it. -You might have noticed that closing brackets are always dedented and that a trailing -comma is always added. Such formatting produces smaller diffs; when you add or remove an -element, it's always just one line. Also, having the closing bracket dedented provides a -clear delimiter between two distinct sections of the code that otherwise share the same -indentation level (like the arguments list and the docstring in the example above). - -If a data structure literal (tuple, list, set, dict) or a line of "from" imports cannot -fit in the allotted length, it's always split into one element per line. This minimizes -diffs as well as enables readers of code to find which commit introduced a particular -entry. This also makes _Black_ compatible with -[isort](../guides/using_black_with_other_tools.md#isort) with the ready-made `black` -profile or manual configuration. - ### Line length You probably noticed the peculiar default line length. _Black_ defaults to 88 characters From d852af71672ce22646017e4ca7a8878ca7bdfe39 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Thu, 15 Sep 2022 21:08:26 +0100 Subject: [PATCH 313/700] Fix mypyc build errors on newer manylinux2014_x86_64 images (#3272) Make sure `gcc` is installed in the build env The mypyc build requires `gcc` to be installed even if it's being built with `clang`, otherwise `clang` fails to find `libgcc`. --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 566462c9b36..a4c9c692085 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,10 +59,8 @@ PIP_NO_BUILD_ISOLATION = "no" [tool.cibuildwheel.linux] before-build = [ "pip install -r .github/mypyc-requirements.txt", - "yum install -y clang", + "yum install -y clang gcc", ] -# Newer images break the builds, not sure why. We'll need to investigate more later. -manylinux-x86_64-image = "quay.io/pypa/manylinux2014_x86_64:2021-11-20-f410d11" [tool.cibuildwheel.linux.environment] BLACK_USE_MYPYC = "1" From 6ae8457a8628345c59fed22c58728ffa258a3e76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 19:35:46 -0400 Subject: [PATCH 314/700] Bump furo from 2022.6.21 to 2022.9.15 in /docs (#3277) 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 121df45e6c2..f4b59fd3cd9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==5.1.1 docutils==0.18.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.6.21 +furo==2022.9.15 From 75d5c0e3fbf5de67b995c80e12229b7525ff6bb9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 22 Sep 2022 23:11:56 -0400 Subject: [PATCH 315/700] Build mypyc wheels for CPython 3.11 (#3276) Bumps cibuildwheel from 2.8.1 to 2.10.0 which has 3.11 building enabled by default. Unfortunately mypyc errors out on 3.11: src/black/files.py:29:9: error: Name "tomllib" already defined (by an import) [no-redef] ... so we have to also hide the fallback import of tomli on older 3.11 alphas from mypy[c]. --- .github/workflows/pypi_upload.yml | 2 +- CHANGES.md | 2 ++ pyproject.toml | 4 ++++ src/black/files.py | 3 ++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index d52f41a4939..ae26a814c9e 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.8.1 + uses: pypa/cibuildwheel@v2.10.0 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" # This isn't supported in pyproject.toml which makes sense (but is annoying). diff --git a/CHANGES.md b/CHANGES.md index 147100c3012..0fa80ad8124 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ +- Faster compiled wheels are now available for CPython 3.11 (#3276) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index a4c9c692085..122a49e004b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ MYPYC_DEBUG_LEVEL = "0" # The dependencies required to build wheels with mypyc aren't specified in # [build-system].requires so we'll have to manage the build environment ourselves. PIP_NO_BUILD_ISOLATION = "no" +# CPython 3.11 wheels aren't available for aiohttp and building a Cython extension +# from source also doesn't work. +AIOHTTP_NO_EXTENSIONS = "1" [tool.cibuildwheel.linux] before-build = [ @@ -69,6 +72,7 @@ MYPYC_DEBUG_LEVEL = "0" PIP_NO_BUILD_ISOLATION = "no" # Black needs Clang to compile successfully on Linux. CC = "clang" +AIOHTTP_NO_EXTENSIONS = "1" [tool.cibuildwheel.windows] # For some reason, (compiled) mypyc is failing to start up with "ImportError: DLL load diff --git a/src/black/files.py b/src/black/files.py index d51c1bc7a90..ed503f5fec7 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -26,7 +26,8 @@ import tomllib except ImportError: # Help users on older alphas - import tomli as tomllib + if not TYPE_CHECKING: + import tomli as tomllib else: import tomli as tomllib From 4c9990023635ece68671324124801f6b75dab2a8 Mon Sep 17 00:00:00 2001 From: Blandes22 <96037855+Blandes22@users.noreply.github.com> Date: Thu, 22 Sep 2022 22:19:31 -0500 Subject: [PATCH 316/700] Make context manager examples in future style docs consistent (#3274) --- docs/the_black_code_style/future_style.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index a17d9a10673..a028a2888ed 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -23,10 +23,10 @@ So _Black_ will eventually format it like this: ```py3 with \ - make_context_manager(1) as cm1, \ - make_context_manager(2) as cm2, \ - make_context_manager(3) as cm3, \ - make_context_manager(4) as cm4 \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ : ... # backslashes and an ugly stranded colon ``` @@ -40,10 +40,10 @@ following way: ```python with contextlib.ExitStack() as exit_stack: - cm1 = exit_stack.enter_context(make_context_manager(1)) - cm2 = exit_stack.enter_context(make_context_manager(2)) - cm3 = exit_stack.enter_context(make_context_manager(3)) - cm4 = exit_stack.enter_context(make_context_manager(4)) + cm1 = exit_stack.enter_context(make_context_manager1()) + cm2 = exit_stack.enter_context(make_context_manager2()) + cm3 = exit_stack.enter_context(make_context_manager3()) + cm4 = exit_stack.enter_context(make_context_manager4()) ... ``` From bfc013ab93d0993a6e24235291dddd4c4ecd64ee Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Fri, 23 Sep 2022 05:23:35 +0200 Subject: [PATCH 317/700] Support version specifiers in GH action (#3265) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 3 +++ action/main.py | 7 ++++--- docs/integrations/github_actions.md | 21 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0fa80ad8124..67d007c21ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,9 @@ +- Update GitHub Action to support use of version specifiers (e.g. `<23`) for Black + version (#3265) + ### Documentation +- Fix a crash when `# fmt: on` is used on a different block level than `# fmt: off` + (#3281) + ### Preview style diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index 50eaeb31e15..3bda5de1774 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -137,9 +137,9 @@ Utilities .. autofunction:: black.comments.is_fmt_on -.. autofunction:: black.comments.contains_fmt_on_at_column +.. autofunction:: black.comments.children_contains_fmt_on -.. autofunction:: black.nodes.first_leaf_column +.. autofunction:: black.nodes.first_leaf_of .. autofunction:: black.linegen.generate_trailers_to_omit diff --git a/src/black/comments.py b/src/black/comments.py index dc58934f9d3..2a4c254ecd9 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -14,11 +14,12 @@ STANDALONE_COMMENT, WHITESPACE, container_of, - first_leaf_column, + first_leaf_of, preceding_leaf, + syms, ) from blib2to3.pgen2 import token -from blib2to3.pytree import Leaf, Node, type_repr +from blib2to3.pytree import Leaf, Node # types LN = Union[Leaf, Node] @@ -230,7 +231,7 @@ def generate_ignored_nodes( return # fix for fmt: on in children - if contains_fmt_on_at_column(container, leaf.column, preview=preview): + if children_contains_fmt_on(container, preview=preview): for child in container.children: if isinstance(child, Leaf) and is_fmt_on(child, preview=preview): if child.type in CLOSING_BRACKETS: @@ -240,10 +241,14 @@ def generate_ignored_nodes( # The alternative is to fail the formatting. yield child return - if contains_fmt_on_at_column(child, leaf.column, preview=preview): + if children_contains_fmt_on(child, preview=preview): return yield child else: + if container.type == token.DEDENT and container.next_sibling is None: + # This can happen when there is no matching `# fmt: on` comment at the + # same level as `# fmt: on`. We need to keep this DEDENT. + return yield container container = container.next_sibling @@ -268,9 +273,7 @@ def _generate_ignored_nodes_from_fmt_skip( for sibling in siblings: yield sibling elif ( - parent is not None - and type_repr(parent.type) == "suite" - and leaf.type == token.NEWLINE + parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE ): # The `# fmt: skip` is on the colon line of the if/while/def/class/... # statements. The ignored nodes should be previous siblings of the @@ -278,7 +281,7 @@ def _generate_ignored_nodes_from_fmt_skip( leaf.prefix = "" ignored_nodes: List[LN] = [] parent_sibling = parent.prev_sibling - while parent_sibling is not None and type_repr(parent_sibling.type) != "suite": + while parent_sibling is not None and parent_sibling.type != syms.suite: ignored_nodes.insert(0, parent_sibling) parent_sibling = parent_sibling.prev_sibling # Special case for `async_stmt` where the ASYNC token is on the @@ -306,17 +309,12 @@ def is_fmt_on(container: LN, preview: bool) -> bool: return fmt_on -def contains_fmt_on_at_column(container: LN, column: int, *, preview: bool) -> bool: - """Determine if children at a given column have formatting switched on.""" +def children_contains_fmt_on(container: LN, *, preview: bool) -> bool: + """Determine if children have formatting switched on.""" for child in container.children: - if ( - isinstance(child, Node) - and first_leaf_column(child) == column - or isinstance(child, Leaf) - and child.column == column - ): - if is_fmt_on(child, preview=preview): - return True + leaf = first_leaf_of(child) + if leaf is not None and is_fmt_on(leaf, preview=preview): + return True return False diff --git a/src/black/nodes.py b/src/black/nodes.py index 8f341ab35d6..aeb2be389c8 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -502,12 +502,14 @@ def container_of(leaf: Leaf) -> LN: return container -def first_leaf_column(node: Node) -> Optional[int]: - """Returns the column of the first leaf child of a node.""" - for child in node.children: - if isinstance(child, Leaf): - return child.column - return None +def first_leaf_of(node: LN) -> Optional[Leaf]: + """Returns the first leaf of the node tree.""" + if isinstance(node, Leaf): + return node + if node.children: + return first_leaf_of(node.children[0]) + else: + return None def is_arith_like(node: LN) -> bool: diff --git a/tests/data/simple_cases/fmtonoff5.py b/tests/data/simple_cases/fmtonoff5.py index 746aa41f4e4..71b1381ed0d 100644 --- a/tests/data/simple_cases/fmtonoff5.py +++ b/tests/data/simple_cases/fmtonoff5.py @@ -34,3 +34,125 @@ def test_func(): return True return False + + +# Regression test for https://github.com/psf/black/issues/2567. +if True: + # fmt: off + for _ in range( 1 ): + # fmt: on + print ( "This won't be formatted" ) + print ( "This won't be formatted either" ) +else: + print ( "This will be formatted" ) + + +# Regression test for https://github.com/psf/black/issues/3184. +class A: + async def call(param): + if param: + # fmt: off + if param[0:4] in ( + "ABCD", "EFGH" + ) : + # fmt: on + print ( "This won't be formatted" ) + + elif param[0:4] in ("ZZZZ",): + print ( "This won't be formatted either" ) + + print ( "This will be formatted" ) + + +# Regression test for https://github.com/psf/black/issues/2985 +class Named(t.Protocol): + # fmt: off + @property + def this_wont_be_formatted ( self ) -> str: ... + +class Factory(t.Protocol): + def this_will_be_formatted ( self, **kwargs ) -> Named: ... + # fmt: on + + +# output + + +# Regression test for https://github.com/psf/black/issues/3129. +setup( + entry_points={ + # fmt: off + "console_scripts": [ + "foo-bar" + "=foo.bar.:main", + # fmt: on + ] # Includes an formatted indentation. + }, +) + + +# Regression test for https://github.com/psf/black/issues/2015. +run( + # fmt: off + [ + "ls", + "-la", + ] + # fmt: on + + path, + check=True, +) + + +# Regression test for https://github.com/psf/black/issues/3026. +def test_func(): + # yapf: disable + if unformatted( args ): + return True + # yapf: enable + elif b: + return True + + return False + + +# Regression test for https://github.com/psf/black/issues/2567. +if True: + # fmt: off + for _ in range( 1 ): + # fmt: on + print ( "This won't be formatted" ) + print ( "This won't be formatted either" ) +else: + print("This will be formatted") + + +# Regression test for https://github.com/psf/black/issues/3184. +class A: + async def call(param): + if param: + # fmt: off + if param[0:4] in ( + "ABCD", "EFGH" + ) : + # fmt: on + print ( "This won't be formatted" ) + + elif param[0:4] in ("ZZZZ",): + print ( "This won't be formatted either" ) + + print("This will be formatted") + + +# Regression test for https://github.com/psf/black/issues/2985 +class Named(t.Protocol): + # fmt: off + @property + def this_wont_be_formatted ( self ) -> str: ... + + +class Factory(t.Protocol): + def this_will_be_formatted(self, **kwargs) -> Named: + ... + + # fmt: on From 4b4680a0a9e06ae926abfbf0d209725de44860a8 Mon Sep 17 00:00:00 2001 From: Stijn de Gooijer Date: Sun, 25 Sep 2022 11:28:43 +0200 Subject: [PATCH 319/700] Make README logo link to docs (#3285) docs: Make README logo link to docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 624d4d755eb..4c761606514 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Black Logo](https://raw.githubusercontent.com/psf/black/main/docs/_static/logo2-readme.png) +[![Black Logo](https://raw.githubusercontent.com/psf/black/main/docs/_static/logo2-readme.png)](https://black.readthedocs.io/en/stable/)

The Uncompromising Code Formatter

From 468ceafca571454fd279f3b428076631fdaffd3d Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sun, 25 Sep 2022 14:54:33 -0700 Subject: [PATCH 320/700] Switch build backend to Hatchling (#3233) This implements PEP 621, obviating the need for `setup.py`, `setup.cfg`, and `MANIFEST.in`. The build backend Hatchling (of which I am a maintainer in the PyPA) is now used as that is the default in the official Python packaging tutorial. Hatchling is available on all the major distribution channels such as Debian, Fedora, and many more. ## Python support The earliest supported Python 3 version of Hatchling is 3.7, therefore I've also set that as the minimum here. Python 3.6 is EOL and other build backends like flit-core and setuptools also dropped support. Python 3.6 accounted for 3-4% of downloads in the last month. ## Plugins Configuration is now completely static with the help of 3 plugins: ### Readme hynek's hatch-fancy-pypi-readme allows for the dynamic construction of the readme which was previously coded up in `setup.py`. Now it's simply: ```toml [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" fragments = [ { path = "README.md" }, { path = "CHANGES.md" }, ] ``` ### Versioning hatch-vcs is currently just a wrapper around setuptools-scm (which despite the legacy naming is actually now decoupled from setuptools): ```toml [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "src/_black_version.py" template = ''' version = "{version}" ''' ``` ### mypyc hatch-mypyc offers many benefits over the existing approach: - No need to manually select files for inclusion - Avoids the need for the current CI workaround for https://github.com/mypyc/mypyc/issues/946 - Intermediate artifacts (like `build/`) from setuptools and mypyc itself no longer clutter the project directory - Runtime dependencies required at build time no longer need to be manually redeclared as this is a built-in option of Hatchling Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- .github/mypyc-requirements.txt | 14 -- .github/workflows/diff_shades.yml | 12 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/pypi_upload.yml | 2 - .github/workflows/test.yml | 2 +- CHANGES.md | 5 + MANIFEST.in | 1 - docs/faq.md | 6 +- docs/usage_and_configuration/the_basics.md | 7 +- pyproject.toml | 135 +++++++++++++++++--- setup.cfg | 3 - setup.py | 141 --------------------- tox.ini | 5 +- 13 files changed, 140 insertions(+), 195 deletions(-) delete mode 100644 .github/mypyc-requirements.txt delete mode 100644 MANIFEST.in delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/mypyc-requirements.txt b/.github/mypyc-requirements.txt deleted file mode 100644 index 352d36c0070..00000000000 --- a/.github/mypyc-requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -mypy == 0.971 - -# A bunch of packages for type information -mypy-extensions >= 0.4.3 -tomli >= 0.10.2 -types-typed-ast >= 1.4.2 -types-dataclasses >= 0.1.3 -typing-extensions > 3.10.0.1 -click >= 8.0.0 -platformdirs >= 2.1.0 - -# And because build isolation is disabled, we'll need to pull this too -setuptools-scm[toml] >= 6.3.1 -wheel diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index fef9637c92f..a126756f102 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -3,10 +3,10 @@ name: diff-shades on: push: branches: [main] - paths: ["src/**", "setup.*", "pyproject.toml", ".github/workflows/*"] + paths: ["src/**", "pyproject.toml", ".github/workflows/*"] pull_request: - paths: ["src/**", "setup.*", "pyproject.toml", ".github/workflows/*"] + paths: ["src/**", "pyproject.toml", ".github/workflows/*"] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} @@ -41,6 +41,7 @@ jobs: needs: configure runs-on: ubuntu-latest env: + HATCH_BUILD_HOOKS_ENABLE: "1" # Clang is less picky with the C code it's given than gcc (and may # generate faster binaries too). CC: clang-12 @@ -64,7 +65,6 @@ jobs: run: | python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip python -m pip install click packaging urllib3 - python -m pip install -r .github/mypyc-requirements.txt # After checking out old revisions, this might not exist so we'll use a copy. cat scripts/diff_shades_gha_helper.py > helper.py git config user.name "diff-shades-gha" @@ -83,8 +83,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: > ${{ matrix.baseline-setup-cmd }} - && python setup.py --use-mypyc bdist_wheel - && python -m pip install dist/*.whl && rm build dist -r + && python -m pip install . - name: Analyze baseline revision if: steps.baseline-cache.outputs.cache-hit != 'true' @@ -97,8 +96,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: > ${{ matrix.target-setup-cmd }} - && python setup.py --use-mypyc bdist_wheel - && python -m pip install dist/*.whl + && python -m pip install . - name: Analyze target revision run: > diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index a2810e25f77..ebb8a9fda9e 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index ae26a814c9e..2c288284444 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -61,8 +61,6 @@ jobs: uses: pypa/cibuildwheel@v2.10.0 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" - # This isn't supported in pyproject.toml which makes sense (but is annoying). - CIBW_PROJECT_REQUIRES_PYTHON: ">=3.6.2" - name: Upload wheels as workflow artifacts uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7cc55d1bf76..372d1fd5d38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] + python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index 48f5035c133..a775c76e4a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ +- Runtime support for Python 3.6 has been removed. Formatting 3.6 code will still be + supported until further notice. + ### Stable style @@ -28,6 +31,8 @@ +- Hatchling is now used as the build backend. This will not have any effect for users + who install Black with its wheels from PyPI. (#3233) - Faster compiled wheels are now available for CPython 3.11 (#3276) ### Parser diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 5e53af336e2..00000000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -prune profiling diff --git a/docs/faq.md b/docs/faq.md index aeb9634789f..8b9ffb0202e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -86,8 +86,8 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Which Python versions does Black support? -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 +Currently the runtime requires Python 3.7-3.11. Formatting is supported for files +containing syntax from Python 3.3 to 3.11. 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. @@ -95,6 +95,8 @@ Support for formatting Python 2 code was removed in version 22.0. While we've ma 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. +Runtime support for 3.6 was removed in version 22.9.0. + ## Why does my linter or typechecker complain after I format my code? Some linters and other tools use magical comments (e.g., `# noqa`, `# type: ignore`) to diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 2dc2a14f91a..aa176c4ba3f 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -204,9 +204,10 @@ code in compliance with many other _Black_ formatted projects. [PEP 518](https://www.python.org/dev/peps/pep-0518/) defines `pyproject.toml` as a configuration file to store build system requirements for Python projects. With the help -of tools like [Poetry](https://python-poetry.org/) or -[Flit](https://flit.readthedocs.io/en/latest/) it can fully replace the need for -`setup.py` and `setup.cfg` files. +of tools like [Poetry](https://python-poetry.org/), +[Flit](https://flit.readthedocs.io/en/latest/), or +[Hatch](https://hatch.pypa.io/latest/) it can fully replace the need for `setup.py` and +`setup.cfg` files. ### Where _Black_ looks for the file diff --git a/pyproject.toml b/pyproject.toml index 122a49e004b..c37702616fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ [tool.black] line-length = 88 -target-version = ['py36', 'py37', 'py38'] +target-version = ['py37', 'py38'] include = '\.pyi?$' extend-exclude = ''' /( @@ -26,18 +26,128 @@ preview = true # NOTE: You don't need this in your own Black configuration. [build-system] -requires = ["setuptools>=45.0", "setuptools_scm[toml]>=6.3.1", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.8.0", "hatch-vcs", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "black" +description = "The uncompromising code formatter." +license = "MIT" +requires-python = ">=3.7" +authors = [ + { name = "Łukasz Langa", email = "lukasz@langa.pl" }, +] +keywords = [ + "automation", + "autopep8", + "formatter", + "gofmt", + "pyfmt", + "rustfmt", + "yapf", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", +] +dependencies = [ + "click>=8.0.0", + "mypy_extensions>=0.4.3", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_full_version < '3.11.0a7'", + "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", + "typing_extensions>=3.10.0.0; python_version < '3.10'", +] +dynamic = ["readme", "version"] + +[project.optional-dependencies] +colorama = ["colorama>=0.4.3"] +uvloop = ["uvloop>=0.15.2"] +d = [ + "aiohttp>=3.7.4", +] +jupyter = [ + "ipython>=7.8.0", + "tokenize-rt>=3.2.0", +] + +[project.scripts] +black = "black:patched_main" +blackd = "blackd:patched_main [d]" + +[project.urls] +Changelog = "https://github.com/psf/black/blob/main/CHANGES.md" +Homepage = "https://github.com/psf/black" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { path = "CHANGES.md" }, +] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/_black_version.py" +template = ''' +version = "{version}" +''' + +[tool.hatch.build.targets.sdist] +exclude = ["/profiling"] + +[tool.hatch.build.targets.wheel] +only-include = ["src"] +sources = ["src"] + +[tool.hatch.build.targets.wheel.hooks.mypyc] +enable-by-default = false +dependencies = [ + "hatch-mypyc>=0.13.0", + "mypy==0.971", + # Required stubs to be removed when the packages support PEP 561 themselves + "types-typed-ast>=1.4.2", +] +require-runtime-dependencies = true +exclude = [ + # There's no good reason for blackd to be compiled. + "/src/blackd", + # Not performance sensitive, so save bytes + compilation time: + "/src/blib2to3/__init__.py", + "/src/blib2to3/pgen2/__init__.py", + "/src/black/output.py", + "/src/black/concurrency.py", + "/src/black/files.py", + "/src/black/report.py", + # Breaks the test suite when compiled (and is also useless): + "/src/black/debug.py", + # Compiled modules can't be run directly and that's a problem here: + "/src/black/__main__.py", +] +options = { debug_level = "0" } [tool.cibuildwheel] build-verbosity = 1 # So these are the environments we target: -# - Python: CPython 3.6+ only +# - Python: CPython 3.7+ only # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS build = "cp3*-*" skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp-*"] -before-build = ["pip install -r .github/mypyc-requirements.txt"] # This is the bare minimum needed to run the test suite. Pulling in the full # test_requirements.txt would download a bunch of other packages not necessary # here and would slow down the testing step a fair bit. @@ -49,7 +159,7 @@ test-extras = ["d"," jupyter"] test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"] [tool.cibuildwheel.environment] -BLACK_USE_MYPYC = "1" +HATCH_BUILD_HOOKS_ENABLE = "1" MYPYC_OPT_LEVEL = "3" MYPYC_DEBUG_LEVEL = "0" # The dependencies required to build wheels with mypyc aren't specified in @@ -61,28 +171,17 @@ AIOHTTP_NO_EXTENSIONS = "1" [tool.cibuildwheel.linux] before-build = [ - "pip install -r .github/mypyc-requirements.txt", "yum install -y clang gcc", ] [tool.cibuildwheel.linux.environment] -BLACK_USE_MYPYC = "1" +HATCH_BUILD_HOOKS_ENABLE = "1" MYPYC_OPT_LEVEL = "3" MYPYC_DEBUG_LEVEL = "0" -PIP_NO_BUILD_ISOLATION = "no" # Black needs Clang to compile successfully on Linux. CC = "clang" AIOHTTP_NO_EXTENSIONS = "1" -[tool.cibuildwheel.windows] -# For some reason, (compiled) mypyc is failing to start up with "ImportError: DLL load -# failed: A dynamic link library (DLL) initialization routine failed." on Windows for -# at least 3.6. Let's just use interpreted mypy[c]. -# See also: https://github.com/mypyc/mypyc/issues/819. -before-build = [ - "pip install -r .github/mypyc-requirements.txt --no-binary mypy" -] - [tool.isort] atomic = true profile = "black" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1a0a217eb91..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[options] -setup_requires = - setuptools_scm[toml]>=6.3.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 2cf455573c9..00000000000 --- a/setup.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (C) 2020 Łukasz Langa -import os -import sys - -from setuptools import find_packages, setup - -assert sys.version_info >= (3, 6, 2), "black requires Python 3.6.2+" -from pathlib import Path # noqa E402 -from typing import List # noqa: E402 - -CURRENT_DIR = Path(__file__).parent -sys.path.insert(0, str(CURRENT_DIR)) # for setuptools.build_meta - - -def get_long_description() -> str: - return ( - (CURRENT_DIR / "README.md").read_text(encoding="utf8") - + "\n\n" - + (CURRENT_DIR / "CHANGES.md").read_text(encoding="utf8") - ) - - -def find_python_files(base: Path) -> List[Path]: - files = [] - for entry in base.iterdir(): - if entry.is_file() and entry.suffix == ".py": - files.append(entry) - elif entry.is_dir(): - files.extend(find_python_files(entry)) - - return files - - -USE_MYPYC = False -# To compile with mypyc, a mypyc checkout must be present on the PYTHONPATH -if len(sys.argv) > 1 and sys.argv[1] == "--use-mypyc": - sys.argv.pop(1) - USE_MYPYC = True -if os.getenv("BLACK_USE_MYPYC", None) == "1": - USE_MYPYC = True - -if USE_MYPYC: - from mypyc.build import mypycify - - src = CURRENT_DIR / "src" - # TIP: filepaths are normalized to use forward slashes and are relative to ./src/ - # before being checked against. - blocklist = [ - # Not performance sensitive, so save bytes + compilation time: - "blib2to3/__init__.py", - "blib2to3/pgen2/__init__.py", - "black/output.py", - "black/concurrency.py", - "black/files.py", - "black/report.py", - # Breaks the test suite when compiled (and is also useless): - "black/debug.py", - # Compiled modules can't be run directly and that's a problem here: - "black/__main__.py", - ] - discovered = [] - # There's no good reason for blackd to be compiled. - discovered.extend(find_python_files(src / "black")) - discovered.extend(find_python_files(src / "blib2to3")) - mypyc_targets = [ - str(p) for p in discovered if p.relative_to(src).as_posix() not in blocklist - ] - - opt_level = os.getenv("MYPYC_OPT_LEVEL", "3") - debug_level = os.getenv("MYPYC_DEBUG_LEVEL", "3") - ext_modules = mypycify( - mypyc_targets, opt_level=opt_level, debug_level=debug_level, verbose=True - ) -else: - ext_modules = [] - -setup( - name="black", - use_scm_version={ - "write_to": "src/_black_version.py", - "write_to_template": 'version = "{version}"\n', - }, - description="The uncompromising code formatter.", - long_description=get_long_description(), - long_description_content_type="text/markdown", - keywords="automation formatter yapf autopep8 pyfmt gofmt rustfmt", - author="Łukasz Langa", - author_email="lukasz@langa.pl", - url="https://github.com/psf/black", - project_urls={"Changelog": "https://github.com/psf/black/blob/main/CHANGES.md"}, - license="MIT", - py_modules=["_black_version"], - ext_modules=ext_modules, - packages=find_packages(where="src"), - package_dir={"": "src"}, - package_data={ - "blib2to3": ["*.txt"], - "black": ["py.typed"], - }, - python_requires=">=3.6.2", - zip_safe=False, - install_requires=[ - "click>=8.0.0", - "platformdirs>=2", - "tomli>=1.1.0; python_full_version < '3.11.0a7'", - "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", - "pathspec>=0.9.0", - "dataclasses>=0.6; python_version < '3.7'", - "typing_extensions>=3.10.0.0; python_version < '3.10'", - "mypy_extensions>=0.4.3", - ], - extras_require={ - "d": ["aiohttp>=3.7.4"], - "colorama": ["colorama>=0.4.3"], - "uvloop": ["uvloop>=0.15.2"], - "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Quality Assurance", - ], - entry_points={ - "console_scripts": [ - "black=black:patched_main", - "blackd=blackd:patched_main [d]", - ] - }, -) diff --git a/tox.ini b/tox.ini index 098f06c9828..4934514264b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310,311,py3},fuzz,run_self +isolated_build = true +envlist = {,ci-}py{37,38,39,310,311,py3},fuzz,run_self [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -96,4 +97,4 @@ setenv = PYTHONPATH = {toxinidir}/src skip_install = True commands = pip install -e .[d] - black --check {toxinidir}/src {toxinidir}/tests {toxinidir}/setup.py + black --check {toxinidir}/src {toxinidir}/tests From 2189bcaac01d9b6289411a75557a23cf4a06b783 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 25 Sep 2022 20:24:18 -0400 Subject: [PATCH 321/700] Fix outdated references to 3.6 and run pyupgrade (#3286) I also missed the accidental removal of the 3.11 classifier in the PR. --- README.md | 5 ++--- docs/getting_started.md | 5 ++--- docs/integrations/editors.md | 2 +- pyproject.toml | 1 + src/black/comments.py | 3 +-- src/black/concurrency.py | 12 ++---------- src/black/trans.py | 2 +- 7 files changed, 10 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4c761606514..b2593024676 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,8 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation -_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run. If you want to format Jupyter Notebooks, install with -`pip install 'black[jupyter]'`. +_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. +If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/getting_started.md b/docs/getting_started.md index fca960915a8..1825f3b5aa3 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -16,9 +16,8 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation -_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run. If you want to format Jupyter Notebooks, install with -`pip install 'black[jupyter]'`. +_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. +If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 318e0e295d0..28c9f48a09f 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -148,7 +148,7 @@ curl https://raw.githubusercontent.com/psf/black/stable/autoload/black.vim -o ~/ Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or Pathogen, and so on. -This plugin **requires Vim 7.0+ built with Python 3.6+ support**. It needs Python 3.6 to +This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to be able to run _Black_ inside the Vim process which is much faster than calling an external command. diff --git a/pyproject.toml b/pyproject.toml index c37702616fc..412e46cbc05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", ] diff --git a/src/black/comments.py b/src/black/comments.py index 2a4c254ecd9..dce83abf1bb 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -270,8 +270,7 @@ def _generate_ignored_nodes_from_fmt_skip( while "\n" not in prev_sibling.prefix and prev_sibling.prev_sibling is not None: prev_sibling = prev_sibling.prev_sibling siblings.insert(0, prev_sibling) - for sibling in siblings: - yield sibling + yield from siblings elif ( parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE ): diff --git a/src/black/concurrency.py b/src/black/concurrency.py index bdc368d5add..10e288f4f93 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -58,12 +58,7 @@ def shutdown(loop: asyncio.AbstractEventLoop) -> None: for task in to_cancel: task.cancel() - if sys.version_info >= (3, 7): - loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) - else: - loop.run_until_complete( - asyncio.gather(*to_cancel, loop=loop, return_exceptions=True) - ) + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) finally: # `concurrent.futures.Future` objects cannot be cancelled once they # are already running. There might be some when the `shutdown()` happened. @@ -191,9 +186,6 @@ async def schedule_formatting( sources_to_cache.append(src) report.done(src, changed) if cancelled: - if sys.version_info >= (3, 7): - await asyncio.gather(*cancelled, return_exceptions=True) - else: - await asyncio.gather(*cancelled, loop=loop, return_exceptions=True) + await asyncio.gather(*cancelled, return_exceptions=True) if sources_to_cache: write_cache(cache, sources_to_cache, mode) diff --git a/src/black/trans.py b/src/black/trans.py index 74b932bb422..7e2d8e67c1a 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1256,7 +1256,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: string_op_leaves = self._get_string_operator_leaves(LL) string_op_leaves_length = ( - sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1 + sum(len(str(prefix_leaf)) for prefix_leaf in string_op_leaves) + 1 if string_op_leaves else 0 ) From af3de081542f66dfb1482dcf2a654b7e1668783c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 25 Sep 2022 20:55:52 -0400 Subject: [PATCH 322/700] Always call freeze_support() if sys.frozen is True (#3275) --- CHANGES.md | 3 +++ src/black/__init__.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a775c76e4a5..6fb099040b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,9 @@ +- Executables made with PyInstaller will no longer crash when formatting several files + at once on macOS. Native x86-64 executables for macOS are available once again. + (#3275) - Hatchling is now used as the build backend. This will not have any effect for users who install Black with its wheels from PyPI. (#3233) - Faster compiled wheels are now available for CPython 3.11 (#3276) diff --git a/src/black/__init__.py b/src/black/__init__.py index ded4a736822..5b8c9749119 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1375,7 +1375,9 @@ def patch_click() -> None: def patched_main() -> None: - if sys.platform == "win32" and getattr(sys, "frozen", False): + # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows + # environments so just assume we always need to call it if frozen. + if getattr(sys, "frozen", False): from multiprocessing import freeze_support freeze_support() From 42fdd1b91f87a92e39ad2676c863328dbf7d194d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 08:35:23 -0400 Subject: [PATCH 323/700] Bump actions/upload-artifact from 2 to 3 (#3289) updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 2c288284444..b9da8b0cfce 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -63,7 +63,7 @@ jobs: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" - name: Upload wheels as workflow artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.name }}-mypyc-wheels path: ./wheelhouse/*.whl From 1f2ad77505337ee68ed5236fc5621cf0690e783c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 09:48:50 -0700 Subject: [PATCH 324/700] Bump sphinx from 5.1.1 to 5.2.1 in /docs (#3288) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.1 to 5.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.1...v5.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 f4b59fd3cd9..efe5cf85b18 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.0 -Sphinx==5.1.1 +Sphinx==5.2.1 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 sphinxcontrib-programoutput==0.17 From 6b42c2b8c9f9bd666120a2c19b8da509fe477f27 Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Mon, 26 Sep 2022 18:45:34 -0300 Subject: [PATCH 325/700] Add option to format Jupyter Notebooks in GitHub Action (#3282) To run the formatter on Jupyter Notebooks, Black must be installed with an extra dependency (`black[jupyter]`). This commit adds an optional argument to install Black with this dependency when using the official GitHub Action [1]. To enable the formatter on Jupyter Notebooks, just add `jupyter: true` as an argument. Feature requested at [2]. [1]: https://black.readthedocs.io/en/stable/integrations/github_actions.html [2]: https://github.com/psf/black/issues/3280 Signed-off-by: Antonio Ossa Guerra --- AUTHORS.md | 1 + CHANGES.md | 2 ++ action.yml | 6 ++++++ action/main.py | 7 ++++++- docs/integrations/github_actions.md | 5 +++++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index 533606240d3..f2d599dd878 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -29,6 +29,7 @@ Multiple contributions by: - [Andrey](mailto:dyuuus@yandex.ru) - [Andy Freeland](mailto:andy@andyfreeland.net) - [Anthony Sottile](mailto:asottile@umich.edu) +- [Antonio Ossa Guerra](mailto:aaossa+black@uc.cl) - [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) - [Arnav Borbornah](mailto:arnavborborah11@gmail.com) - [Artem Malyshev](mailto:proofit404@gmail.com) diff --git a/CHANGES.md b/CHANGES.md index 6fb099040b3..c96d9a6e492 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,6 +58,8 @@ +- Update GitHub Action to support formatting of Jupyter Notebook files via a `jupyter` + option (#3282) - Update GitHub Action to support use of version specifiers (e.g. `<23`) for Black version (#3265) diff --git a/action.yml b/action.yml index cfa6ef9fb7e..35705e99414 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,11 @@ inputs: description: "Source to run Black. Default: '.'" required: false default: "." + jupyter: + description: + "Set this option to true to include Jupyter Notebook files. Default: false" + required: false + default: false black_args: description: "[DEPRECATED] Black input arguments." required: false @@ -38,6 +43,7 @@ runs: # TODO: Remove once https://github.com/actions/runner/issues/665 is fixed. INPUT_OPTIONS: ${{ inputs.options }} INPUT_SRC: ${{ inputs.src }} + INPUT_JUPYTER: ${{ inputs.jupyter }} INPUT_BLACK_ARGS: ${{ inputs.black_args }} INPUT_VERSION: ${{ inputs.version }} pythonioencoding: utf-8 diff --git a/action/main.py b/action/main.py index 03228cb13e8..ff9d4112aed 100644 --- a/action/main.py +++ b/action/main.py @@ -9,6 +9,7 @@ ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin") OPTIONS = os.getenv("INPUT_OPTIONS", default="") SRC = os.getenv("INPUT_SRC", default="") +JUPYTER = os.getenv("INPUT_JUPYTER") == "true" BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="") VERSION = os.getenv("INPUT_VERSION", default="") @@ -17,7 +18,11 @@ version_specifier = VERSION if VERSION and VERSION[0] in "0123456789": version_specifier = f"=={VERSION}" -req = f"black[colorama]{version_specifier}" +if JUPYTER: + extra_deps = "[colorama,jupyter]" +else: + extra_deps = "[colorama]" +req = f"black{extra_deps}{version_specifier}" pip_proc = run( [str(ENV_BIN / "python"), "-m", "pip", "install", req], stdout=PIPE, diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index d77b9693678..12bcb21fee6 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -39,6 +39,10 @@ or just the version number if you want an exact version. The action defaults to latest release available on PyPI. Only versions available from PyPI are supported, so no commit SHAs or branch names. +If you want to include Jupyter Notebooks, _Black_ must be installed with the `jupyter` +extra. Installing the extra and including Jupyter Notebook files can be configured via +`jupyter` (default is `false`). + You can also configure the arguments passed to _Black_ via `options` (defaults to `'--check --diff'`) and `src` (default is `'.'`) @@ -49,6 +53,7 @@ Here's an example configuration: with: options: "--check --verbose" src: "./src" + jupyter: true version: "21.5b1" ``` From 096806ee269bb6823860e6fb3a33f25d79c6b6aa Mon Sep 17 00:00:00 2001 From: Ray Bell Date: Wed, 28 Sep 2022 21:44:16 -0400 Subject: [PATCH 326/700] Mention CHANGES.md in PR template explicitly (#3295) This makes the location more explicit which hopefully makes the PR process smoother for other first time contributors. Co-authored-by: Jelle Zijlstra --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 833cd164134..a039718cd70 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,7 +18,7 @@ Tests are required for bugfixes and new features. Documentation changes are necessary for formatting and most enhancement changes. --> -- [ ] Add a CHANGELOG entry if necessary? +- [ ] Add an entry in `CHANGES.md` if necessary? - [ ] Add / update tests if necessary? - [ ] Add new / update outdated documentation? From ddb99241b583f45e01df622c0d8f119aecd0188e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:19:08 -0400 Subject: [PATCH 327/700] Bump pypa/cibuildwheel from 2.10.0 to 2.10.2 (#3290) updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index b9da8b0cfce..76bcd33b55e 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.10.0 + uses: pypa/cibuildwheel@v2.10.2 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 141291a1d86d43158da89d0254b7c2cc79609679 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:56:28 -0400 Subject: [PATCH 328/700] Enable build isolation under CIWB (#3297) No idea how this is still here after the Hatchling PR, but it is no longer useful and is breaking the build. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 412e46cbc05..554d7d07bf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,9 +163,6 @@ test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"] HATCH_BUILD_HOOKS_ENABLE = "1" MYPYC_OPT_LEVEL = "3" MYPYC_DEBUG_LEVEL = "0" -# The dependencies required to build wheels with mypyc aren't specified in -# [build-system].requires so we'll have to manage the build environment ourselves. -PIP_NO_BUILD_ISOLATION = "no" # CPython 3.11 wheels aren't available for aiohttp and building a Cython extension # from source also doesn't work. AIOHTTP_NO_EXTENSIONS = "1" From 956bf3962edff284d05ad42576bac7e74ae8788d Mon Sep 17 00:00:00 2001 From: Ray Bell Date: Sun, 2 Oct 2022 12:26:45 -0400 Subject: [PATCH 329/700] Add .ipynb_checkpoints to DEFAULT_EXCLUDES (#3293) Jupyter creates a checkpoint file every single time you create an .ipynb file, and then it updates the checkpoint file every single time you manually save your progress for the initial .ipynb. These checkpoints are stored in a directory named `.ipynb_checkpoints`. Co-authored-by: Batuhan Taskaya --- CHANGES.md | 2 ++ src/black/const.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c96d9a6e492..d3ba53fd05f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,8 @@ +- `.ipynb_checkpoints` directories are now excluded by default (#3293) + ### Packaging diff --git a/src/black/const.py b/src/black/const.py index 03afc96e8d6..0e13f31517d 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|__pypackages__)/" # noqa: B950 +DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|\.ipynb_checkpoints|_build|buck-out|build|dist|__pypackages__)/" # noqa: B950 DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$" STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" From b1077aa14ee6afc90aac15549a1f5d0aff4fd524 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 02:06:06 -0500 Subject: [PATCH 330/700] Bump myst-parser from 0.18.0 to 0.18.1 in /docs (#3303) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 0.18.0 to 0.18.1. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v0.18.0...v0.18.1) --- updated-dependencies: - dependency-name: myst-parser dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 efe5cf85b18..4facf38f94f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.18.0 +myst-parser==0.18.1 Sphinx==5.2.1 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 From 980997f215d25deb27e03ea704258f62199b8a5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 02:08:20 -0500 Subject: [PATCH 331/700] Bump furo from 2022.9.15 to 2022.9.29 in /docs (#3304) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.9.15 to 2022.9.29. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.09.15...2022.09.29) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cooper Lees --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 4facf38f94f..b2e4a654752 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==5.2.1 docutils==0.18.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 -furo==2022.9.15 +furo==2022.9.29 From 1a20c4d4874f912822f6a42cb61816330a4f6508 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 02:15:59 -0500 Subject: [PATCH 332/700] Bump sphinx from 5.2.1 to 5.2.3 in /docs (#3305) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.1 to 5.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.1...v5.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 b2e4a654752..a3c6da0fb44 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.1 -Sphinx==5.2.1 +Sphinx==5.2.3 # Older versions break Sphinx even though they're declared to be supported. docutils==0.18.1 sphinxcontrib-programoutput==0.17 From 27d7ea43eb127cc5189a724a7d194d94ba312861 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 09:36:44 -0700 Subject: [PATCH 333/700] Bump docutils from 0.18.1 to 0.19 in /docs (#3161) Bumps [docutils](https://docutils.sourceforge.io/) from 0.18.1 to 0.19. --- updated-dependencies: - dependency-name: docutils dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index a3c6da0fb44..3c4b43511f6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ myst-parser==0.18.1 Sphinx==5.2.3 # Older versions break Sphinx even though they're declared to be supported. -docutils==0.18.1 +docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.0 furo==2022.9.29 From 0359b85b5800dd77f8f1cfaa88ca8ab8215df685 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> Date: Wed, 5 Oct 2022 06:10:11 +1000 Subject: [PATCH 334/700] Preserve crlf line endings in blackd (#3257) Co-authored-by: KotlinIsland --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 5 +++++ src/blackd/__init__.py | 7 +++++++ tests/test_black.py | 11 +++++++++++ tests/test_blackd.py | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d3ba53fd05f..4ff181674b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,7 +54,7 @@ ### _Blackd_ - +- Windows style (CRLF) newlines will be preserved (#3257). ### Integrations diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 3db49e2ba01..59d79c4cd0e 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -406,6 +406,11 @@ file that are not enforced yet but might be in a future version of the formatter - use variable annotations instead of type comments, even for stubs that target older versions of Python. +### Line endings + +_Black_ will normalize line endings (`\n` or `\r\n`) based on the first line ending of +the file. + ## Pragmatism Early versions of _Black_ used to be absolutist in some respects. They took after its diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index e52a9917cf3..6bbc7c52086 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -133,6 +133,13 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode) ) + # Preserve CRLF line endings + if req_str[req_str.find("\n") - 1] == "\r": + formatted_str = formatted_str.replace("\n", "\r\n") + # If, after swapping line endings, nothing changed, then say so + if formatted_str == req_str: + raise black.NothingChanged + # Only output the diff in the HTTP response only_diff = bool(request.headers.get(DIFF_HEADER, False)) if only_diff: diff --git a/tests/test_black.py b/tests/test_black.py index abd4d00b8e8..96e6f1e6945 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1286,6 +1286,17 @@ def test_preserves_line_endings_via_stdin(self) -> None: if nl == "\n": self.assertNotIn(b"\r\n", output) + def test_normalize_line_endings(self) -> None: + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "test.py" + for data, expected in ( + (b"c\r\nc\n ", b"c\r\nc\r\n"), + (b"l\nl\r\n ", b"l\nl\n"), + ): + test_file.write_bytes(data) + ff(test_file, write_back=black.WriteBack.YES) + self.assertEqual(test_file.read_bytes(), expected) + def test_assert_equivalent_different_asts(self) -> None: with self.assertRaises(AssertionError): black.assert_equivalent("{}", "None") diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 511bd86441d..db9a1652f8c 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -209,3 +209,20 @@ async def test_cors_headers_present(self) -> None: response = await self.client.post("/", headers={"Origin": "*"}) self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) + + @unittest_run_loop + async def test_preserves_line_endings(self) -> None: + for data in (b"c\r\nc\r\n", b"l\nl\n"): + # test preserved newlines when reformatted + response = await self.client.post("/", data=data + b" ") + self.assertEqual(await response.text(), data.decode()) + # test 204 when no change + response = await self.client.post("/", data=data) + self.assertEqual(response.status, 204) + + @unittest_run_loop + async def test_normalizes_line_endings(self) -> None: + for data, expected in ((b"c\r\nc\n", "c\r\nc\r\n"), (b"l\nl\r\n", "l\nl\n")): + response = await self.client.post("/", data=data) + self.assertEqual(await response.text(), expected) + self.assertEqual(response.status, 200) From 4da0851809e024760d3861ff43309125de34157a Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Thu, 6 Oct 2022 19:17:32 -0300 Subject: [PATCH 335/700] Add option to skip the first line of source code (#3299) * Add option to skip the first line in source file This commit adds a CLi option to skip the first line in the source files, just like the Cpython command line allows [1]. By enabling the flag, using `-x` or `--skip-source-first-line`, the first line is removed temporarilly while the remaining contents are formatted. The first line is added back before returning the formatted output. [1]: https://docs.python.org/dev/using/cmdline.html#cmdoption-x Signed-off-by: Antonio Ossa Guerra * Add tests for `--skip-source-first-line` option When the flag is disabled (default), black formats the entire source file, as in every line. In the other hand, if the flag is enabled, by using `-x` or `--skip-source-first-line`, the first line is retained while the rest of the source is formatted and then is added back. These tests use an empty Python file that contains invalid syntax in its first line (`invalid_header.py`, at `miscellaneous/`). First, Black is invoked without enabling the flag which should result in an exit code different than 0. When the flag is enabled, Black is expected to return a successful exit code and the header is expected to be retained (even if its not valid Python syntax). Signed-off-by: Antonio Ossa Guerra * Support skip source first line option for blackd The recently added option can be added as an acceptable header for blackd. The arguments are passed in such a way that using the new header will activate the skip source first line behaviour as expected Signed-off-by: Antonio Ossa Guerra * Add skip source first line option to blackd docs The new option can be passed to blackd as a header. This commit updates the blackd docs to include the new header. Signed-off-by: Antonio Ossa Guerra * Update CHANGES.md Include the new Black option to skip the first line of source code in the configuration section Signed-off-by: Antonio Ossa Guerra * Update skip first line test including valid syntax Including valid Python syntax help us make sure that the file is still actually valid after skipping the first line of the source file (which contains invalid Python syntax) Signed-off-by: Antonio Ossa Guerra * Skip first source line at `format_file_in_place` Instead of skipping the first source line at `format_file_contents`, do it before. This allow us to find the correct newline and encoding on the actual source code (everything that's after the header). This change is also applied at Blackd: take the header before passing the source to `format_file_contents` and put the header back once we get the formatted result. Signed-off-by: Antonio Ossa Guerra * Test output newlines when skipping first line When skipping the first line of source code, the reference newline must be taken from the second line of the file instead of the first one, in case that the file mixes more than one kind of newline character Signed-off-by: Antonio Ossa Guerra * Test that Blackd also skips first line correctly Simliarly to the Black tests, we first compare that Blackd fails when the first line is invalid Python syntax and then check that the result is the expected when tha flag is activated Signed-off-by: Antonio Ossa Guerra * Use the content encoding to decode the header When decoding the header to put it back at the top of the contents of the file, use the same encoding used in the content. This should be a better "guess" that using the default value Signed-off-by: Antonio Ossa Guerra --- CHANGES.md | 2 ++ .../black_as_a_server.md | 3 +++ src/black/__init__.py | 13 ++++++++++ src/black/mode.py | 2 ++ src/blackd/__init__.py | 16 +++++++++++++ tests/data/miscellaneous/invalid_header.py | 2 ++ tests/test_black.py | 24 +++++++++++++++++++ tests/test_blackd.py | 14 +++++++++++ 8 files changed, 76 insertions(+) create mode 100644 tests/data/miscellaneous/invalid_header.py diff --git a/CHANGES.md b/CHANGES.md index 4ff181674b1..ffdbd9c7aff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,8 @@ - `.ipynb_checkpoints` directories are now excluded by default (#3293) +- Add `--skip-source-first-line` / `-x` option to ignore the first line of source code + while formatting (#3299) ### Packaging diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index a2d4252109a..f24fb34d915 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -50,6 +50,9 @@ is rejected with `HTTP 501` (Not Implemented). The headers controlling how source code is formatted are: - `X-Line-Length`: corresponds to the `--line-length` command line flag. +- `X-Skip-Source-First-Line`: corresponds to the `--skip-source-first-line` command line + flag. If present and its value is not an empty string, the first line of the source + code will be ignored. - `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization` command line flag. If present and its value is not the empty string, no string normalization will be performed. diff --git a/src/black/__init__.py b/src/black/__init__.py index 5b8c9749119..afd71e51916 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -248,6 +248,12 @@ def validate_regex( ), default=[], ) +@click.option( + "-x", + "--skip-source-first-line", + is_flag=True, + help="Skip the first line of the source code.", +) @click.option( "-S", "--skip-string-normalization", @@ -428,6 +434,7 @@ def main( # noqa: C901 pyi: bool, ipynb: bool, python_cell_magics: Sequence[str], + skip_source_first_line: bool, skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, @@ -528,6 +535,7 @@ def main( # noqa: C901 line_length=line_length, is_pyi=pyi, is_ipynb=ipynb, + skip_source_first_line=skip_source_first_line, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, @@ -790,7 +798,10 @@ def format_file_in_place( mode = replace(mode, is_ipynb=True) then = datetime.utcfromtimestamp(src.stat().st_mtime) + header = b"" with open(src, "rb") as buf: + if mode.skip_source_first_line: + header = buf.readline() src_contents, encoding, newline = decode_bytes(buf.read()) try: dst_contents = format_file_contents(src_contents, fast=fast, mode=mode) @@ -800,6 +811,8 @@ def format_file_in_place( raise ValueError( f"File '{src}' cannot be parsed as valid Jupyter notebook." ) from None + src_contents = header.decode(encoding) + src_contents + dst_contents = header.decode(encoding) + dst_contents if write_back == WriteBack.YES: with open(src, "w", encoding=encoding, newline=newline) as f: diff --git a/src/black/mode.py b/src/black/mode.py index 6c0847e8bcc..e3c36450ed1 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -170,6 +170,7 @@ class Mode: string_normalization: bool = True is_pyi: bool = False is_ipynb: bool = False + skip_source_first_line: bool = False magic_trailing_comma: bool = True experimental_string_processing: bool = False python_cell_magics: Set[str] = field(default_factory=set) @@ -208,6 +209,7 @@ def get_cache_key(self) -> str: str(int(self.string_normalization)), str(int(self.is_pyi)), str(int(self.is_ipynb)), + str(int(self.skip_source_first_line)), str(int(self.magic_trailing_comma)), str(int(self.experimental_string_processing)), str(int(self.preview)), diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 6bbc7c52086..ba4750b8298 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -30,6 +30,7 @@ PROTOCOL_VERSION_HEADER = "X-Protocol-Version" LINE_LENGTH_HEADER = "X-Line-Length" PYTHON_VARIANT_HEADER = "X-Python-Variant" +SKIP_SOURCE_FIRST_LINE = "X-Skip-Source-First-Line" SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" PREVIEW = "X-Preview" @@ -40,6 +41,7 @@ PROTOCOL_VERSION_HEADER, LINE_LENGTH_HEADER, PYTHON_VARIANT_HEADER, + SKIP_SOURCE_FIRST_LINE, SKIP_STRING_NORMALIZATION_HEADER, SKIP_MAGIC_TRAILING_COMMA, PREVIEW, @@ -111,6 +113,9 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: skip_magic_trailing_comma = bool( request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False) ) + skip_source_first_line = bool( + request.headers.get(SKIP_SOURCE_FIRST_LINE, False) + ) preview = bool(request.headers.get(PREVIEW, False)) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": @@ -119,6 +124,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: target_versions=versions, is_pyi=pyi, line_length=line_length, + skip_source_first_line=skip_source_first_line, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, preview=preview, @@ -128,6 +134,12 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: req_str = req_bytes.decode(charset) then = datetime.utcnow() + header = "" + if skip_source_first_line: + first_newline_position: int = req_str.find("\n") + 1 + header = req_str[:first_newline_position] + req_str = req_str[first_newline_position:] + loop = asyncio.get_event_loop() formatted_str = await loop.run_in_executor( executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode) @@ -140,6 +152,10 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: if formatted_str == req_str: raise black.NothingChanged + # Put the source first line back + req_str = header + req_str + formatted_str = header + formatted_str + # Only output the diff in the HTTP response only_diff = bool(request.headers.get(DIFF_HEADER, False)) if only_diff: diff --git a/tests/data/miscellaneous/invalid_header.py b/tests/data/miscellaneous/invalid_header.py new file mode 100644 index 00000000000..fb49e2f93e7 --- /dev/null +++ b/tests/data/miscellaneous/invalid_header.py @@ -0,0 +1,2 @@ +This is not valid Python syntax +y = "This is valid syntax" diff --git a/tests/test_black.py b/tests/test_black.py index 96e6f1e6945..5d0175d9d66 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -341,6 +341,30 @@ def test_string_quotes(self) -> None: black.assert_equivalent(source, not_normalized) black.assert_stable(source, not_normalized, mode=mode) + def test_skip_source_first_line(self) -> None: + source, _ = read_data("miscellaneous", "invalid_header") + tmp_file = Path(black.dump_to_file(source)) + # Full source should fail (invalid syntax at header) + self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123) + # So, skipping the first line should work + result = BlackRunner().invoke( + black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"] + ) + self.assertEqual(result.exit_code, 0) + with open(tmp_file, encoding="utf8") as f: + actual = f.read() + self.assertFormatEqual(source, actual) + + def test_skip_source_first_line_when_mixing_newlines(self) -> None: + code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n" + expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n" + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "skip_header.py" + test_file.write_bytes(code_mixing_newlines) + mode = replace(DEFAULT_MODE, skip_source_first_line=True) + ff(test_file, mode=mode, write_back=black.WriteBack.YES) + self.assertEqual(test_file.read_bytes(), expected) + def test_skip_magic_trailing_comma(self) -> None: source, _ = read_data("simple_cases", "expression") expected, _ = read_data( diff --git a/tests/test_blackd.py b/tests/test_blackd.py index db9a1652f8c..5b6461f7685 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -177,6 +177,20 @@ async def test_blackd_invalid_line_length(self) -> None: ) self.assertEqual(response.status, 400) + @unittest_run_loop + async def test_blackd_skip_first_source_line(self) -> None: + invalid_first_line = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n" + expected_result = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n" + response = await self.client.post("/", data=invalid_first_line) + self.assertEqual(response.status, 400) + response = await self.client.post( + "/", + data=invalid_first_line, + headers={blackd.SKIP_SOURCE_FIRST_LINE: "true"}, + ) + self.assertEqual(response.status, 200) + self.assertEqual(await response.read(), expected_result) + @unittest_run_loop async def test_blackd_preview(self) -> None: response = await self.client.post( From 27d20144a7517594e24a1649451177b2a11284be Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 6 Oct 2022 15:33:51 -0700 Subject: [PATCH 336/700] Prepare release 22.10.0 (#3311) --- CHANGES.md | 72 +++++++++++++-------- docs/faq.md | 2 +- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ffdbd9c7aff..3a8cf4d6af5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,42 +6,22 @@ -- Runtime support for Python 3.6 has been removed. Formatting 3.6 code will still be - supported until further notice. - ### Stable style -- Fix a crash when `# fmt: on` is used on a different block level than `# fmt: off` - (#3281) - ### Preview style -- Fix a crash when formatting some dicts with parenthesis-wrapped long string keys - (#3262) - ### Configuration -- `.ipynb_checkpoints` directories are now excluded by default (#3293) -- Add `--skip-source-first-line` / `-x` option to ignore the first line of source code - while formatting (#3299) - ### Packaging -- Executables made with PyInstaller will no longer crash when formatting several files - at once on macOS. Native x86-64 executables for macOS are available once again. - (#3275) -- Hatchling is now used as the build backend. This will not have any effect for users - who install Black with its wheels from PyPI. (#3233) -- Faster compiled wheels are now available for CPython 3.11 (#3276) - ### Parser @@ -56,22 +36,61 @@ ### _Blackd_ -- Windows style (CRLF) newlines will be preserved (#3257). + ### Integrations -- Update GitHub Action to support formatting of Jupyter Notebook files via a `jupyter` - option (#3282) -- Update GitHub Action to support use of version specifiers (e.g. `<23`) for Black - version (#3265) - ### Documentation +## 22.10.0 + +### Highlights + +- Runtime support for Python 3.6 has been removed. Formatting 3.6 code will still be + supported until further notice. + +### Stable style + +- Fix a crash when `# fmt: on` is used on a different block level than `# fmt: off` + (#3281) + +### Preview style + +- Fix a crash when formatting some dicts with parenthesis-wrapped long string keys + (#3262) + +### Configuration + +- `.ipynb_checkpoints` directories are now excluded by default (#3293) +- Add `--skip-source-first-line` / `-x` option to ignore the first line of source code + while formatting (#3299) + +### Packaging + +- Executables made with PyInstaller will no longer crash when formatting several files + at once on macOS. Native x86-64 executables for macOS are available once again. + (#3275) +- Hatchling is now used as the build backend. This will not have any effect for users + who install Black with its wheels from PyPI. (#3233) +- Faster compiled wheels are now available for CPython 3.11 (#3276) + +### _Blackd_ + +- Windows style (CRLF) newlines will be preserved (#3257). + +### Integrations + +- Vim plugin: add flag (`g:black_preview`) to enable/disable the preview style (#3246) +- Update GitHub Action to support formatting of Jupyter Notebook files via a `jupyter` + option (#3282) +- Update GitHub Action to support use of version specifiers (e.g. `<23`) for Black + version (#3265) + ## 22.8.0 ### Highlights @@ -126,7 +145,6 @@ - Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194) - Docker: changed to a /opt/venv installation + added to PATH to be available to non-root users (#3202) -- Vim plugin: add flag (`g:black_preview`) to enable/disable the preview style (#3246) ### Output diff --git a/docs/faq.md b/docs/faq.md index 8b9ffb0202e..bc9deccb756 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -95,7 +95,7 @@ Support for formatting Python 2 code was removed in version 22.0. While we've ma 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. -Runtime support for 3.6 was removed in version 22.9.0. +Runtime support for 3.6 was removed in version 22.10.0. ## Why does my linter or typechecker complain after I format my code? diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 31d0df27273..4189b5c4b06 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: 22.8.0 + rev: 22.10.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 aa176c4ba3f..20aa956dd85 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 22.8.0 +black, version 22.10.0 ``` An option to require a specific version to be running is also provided. From b60b85b234d6a575f636d0a125478115f993c90c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 6 Oct 2022 17:37:37 -0700 Subject: [PATCH 337/700] Remove redundant 3.6 code and bump mypy's python_version to 3.7 (#3313) --- autoload/black.vim | 4 ++-- mypy.ini | 7 +------ src/black/__init__.py | 4 ++-- src/black/concurrency.py | 6 +----- src/black/parsing.py | 3 +-- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/autoload/black.vim b/autoload/black.vim index e87a1e4edfa..eec44637950 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -57,8 +57,8 @@ def _get_virtualenv_site_packages(venv_path, pyver): def _initialize_black_env(upgrade=False): pyver = sys.version_info[:3] - if pyver < (3, 6, 2): - print("Sorry, Black requires Python 3.6.2+ to run.") + if pyver < (3, 7): + print("Sorry, Black requires Python 3.7+ to run.") return False from pathlib import Path diff --git a/mypy.ini b/mypy.ini index 4811cc0be76..58bb7536173 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent # results. -python_version=3.6 +python_version=3.7 mypy_path=src @@ -19,11 +19,6 @@ no_implicit_reexport = False # to avoid 'em in the first place. warn_unreachable=True -[mypy-black] -# The following is because of `patch_click()`. Remove when -# we drop Python 3.6 support. -warn_unused_ignores=False - [mypy-blib2to3.driver.*] ignore_missing_imports = True diff --git a/src/black/__init__.py b/src/black/__init__.py index afd71e51916..5293796aea1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1382,9 +1382,9 @@ def patch_click() -> None: for module in modules: if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None # type: ignore + module._verify_python3_env = lambda: None if hasattr(module, "_verify_python_env"): - module._verify_python_env = lambda: None # type: ignore + module._verify_python_env = lambda: None def patched_main() -> None: diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 10e288f4f93..1598f51e43f 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -47,12 +47,8 @@ def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: def shutdown(loop: asyncio.AbstractEventLoop) -> None: """Cancel all pending tasks on `loop`, wait for them, and close the loop.""" try: - if sys.version_info[:2] >= (3, 7): - all_tasks = asyncio.all_tasks - else: - all_tasks = asyncio.Task.all_tasks # This part is borrowed from asyncio/runners.py in Python 3.7b2. - to_cancel = [task for task in all_tasks(loop) if not task.done()] + to_cancel = [task for task in asyncio.all_tasks(loop) if not task.done()] if not to_cancel: return diff --git a/src/black/parsing.py b/src/black/parsing.py index 64c0b1e3018..f7d9fdc2f13 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -27,8 +27,7 @@ try: from typed_ast import ast3 except ImportError: - # Either our python version is too low, or we're on pypy - if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY): + if sys.version_info < (3, 8) and not _IS_PYPY: print( "The typed_ast package is required but not installed.\n" "You can upgrade to Python 3.8+ or install typed_ast with\n" From 1c786ee6273377ac68a98483c1e6e7cd81bde332 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 11 Oct 2022 02:54:09 +0300 Subject: [PATCH 338/700] Add support for named exprs inside function calls as gen-exps (#3327) --- CHANGES.md | 4 ++++ src/blib2to3/Grammar.txt | 2 +- tests/data/py_310/pep_572_py310.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3a8cf4d6af5..ba9f4c06f28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,10 @@ +- Parsing support has been added for walruses inside generator expression that are + passed as function args (for example, + `any(match := my_re.match(text) for text in texts)`) (#3327). + ### Performance diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index ac7ad7643ff..bd8a452a386 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -186,7 +186,7 @@ arglist: argument (',' argument)* [','] # multiple (test comp_for) arguments are blocked; keyword unpackings # that precede iterable unpackings are blocked; etc. argument: ( test [comp_for] | - test ':=' test | + test ':=' test [comp_for] | test 'as' test | test '=' asexpr_test | '**' test | diff --git a/tests/data/py_310/pep_572_py310.py b/tests/data/py_310/pep_572_py310.py index 2aef589ce8d..cb82b2d23f8 100644 --- a/tests/data/py_310/pep_572_py310.py +++ b/tests/data/py_310/pep_572_py310.py @@ -2,3 +2,14 @@ x[a:=0] x[a:=0, b:=1] x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) From d923945513d8f679f43aec44aa8b777e5f9aab14 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 10 Oct 2022 16:54:49 -0700 Subject: [PATCH 339/700] Fix license metadata to follow PEP 621 (#3326) --- AUTHORS.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index f2d599dd878..a635e8c3c92 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -24,6 +24,7 @@ Multiple contributions by: - [Alex Vandiver](mailto:github@chmrr.net) - [Allan Simon](mailto:allan.simon@supinfo.com) - Anders-Petter Ljungquist +- [Amethyst Reese](mailto:amy@n7.gg) - [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) - [Andrew Zhou](mailto:andrewfzhou@gmail.com) - [Andrey](mailto:dyuuus@yandex.ru) diff --git a/pyproject.toml b/pyproject.toml index 554d7d07bf3..329adaa65d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ build-backend = "hatchling.build" [project] name = "black" description = "The uncompromising code formatter." -license = "MIT" +license = { text = "MIT" } requires-python = ">=3.7" authors = [ { name = "Łukasz Langa", email = "lukasz@langa.pl" }, From f16333e78ba77ab29ab0b75e10891e6c65c6da7e Mon Sep 17 00:00:00 2001 From: nn <45516943+NNRepos@users.noreply.github.com> Date: Wed, 12 Oct 2022 00:34:37 +0300 Subject: [PATCH 340/700] remove unreachable code (#3328) fixes #3321 --- src/black/parsing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/black/parsing.py b/src/black/parsing.py index f7d9fdc2f13..762934b8869 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -165,8 +165,6 @@ def parse_single_version( # comments separately. return ast3.parse(src, filename, feature_version=version[1]) - raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!") - def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: # TODO: support Python 4+ ;) From 575220f460fc3a5eeb05673e9cc7d8e80b6b7147 Mon Sep 17 00:00:00 2001 From: jlplenio Date: Sat, 15 Oct 2022 20:44:02 +0200 Subject: [PATCH 341/700] Clarify check argument is needed for github action workflow (#3325) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/integrations/github_actions.md | 4 +++- docs/usage_and_configuration/the_basics.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index 12bcb21fee6..ebfcc2d95a2 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -44,7 +44,9 @@ extra. Installing the extra and including Jupyter Notebook files can be configur `jupyter` (default is `false`). You can also configure the arguments passed to _Black_ via `options` (defaults to -`'--check --diff'`) and `src` (default is `'.'`) +`'--check --diff'`) and `src` (default is `'.'`). Please note that the +[`--check` flag](labels/exit-code) is required so that the workflow fails if _Black_ +finds files that need to be formatted. Here's an example configuration: diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 20aa956dd85..80897532a68 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -76,6 +76,8 @@ _Black_ to just tell you what it _would_ do without actually rewriting the Pytho There's two variations to this mode that are independently enabled by their respective flags. Both variations can be enabled at once. +(labels/exit-code)= + #### Exit code Passing `--check` will make _Black_ exit with: From f22273a72b3f1c15085f2d4a43e8d785bf48c822 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:12:41 -0400 Subject: [PATCH 342/700] Bump sphinx from 5.2.3 to 5.3.0 in /docs (#3333) updated-dependencies: - dependency-name: sphinx 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 3c4b43511f6..c2342a35d7b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==0.18.1 -Sphinx==5.2.3 +Sphinx==5.3.0 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 From 26de5f91c37b825214f4ee0a9096f1cd34212e76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:50:14 -0700 Subject: [PATCH 343/700] Bump peter-evans/find-comment from 2.0.0 to 2.0.1 (#3353) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/1769778a0c5bd330272d749d12c036d65e70d39d...b657a70ff16d17651703a84bee1cb9ad9d2be2ea) --- updated-dependencies: - dependency-name: peter-evans/find-comment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index a5d213875c7..f06541b45d5 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@1769778a0c5bd330272d749d12c036d65e70d39d + uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From fbc5136aa003d806c3af177de770bd58544fe463 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 11:40:19 -0700 Subject: [PATCH 344/700] Bump peter-evans/create-or-update-comment from 2.0.0 to 2.0.1 (#3354) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/c9fcb64660bc90ec1cc535646af190c992007c32...2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index f06541b45d5..9a22ec5f0c3 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -41,7 +41,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@c9fcb64660bc90ec1cc535646af190c992007c32 + uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From 4abc0399b527c50369c448e00d6e204af74026e5 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 25 Oct 2022 18:03:24 -0700 Subject: [PATCH 345/700] Enforce empty lines before classes/functions with sticky leading comments. (#3302) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + .../reference/reference_classes.rst | 22 +- docs/the_black_code_style/future_style.md | 49 +++- src/black/__init__.py | 21 +- src/black/lines.py | 86 +++++- src/black/mode.py | 1 + tests/data/preview/comments9.py | 254 ++++++++++++++++++ tests/data/preview/remove_await_parens.py | 1 + tests/data/simple_cases/comments5.py | 6 +- ...ocstring_no_extra_empty_line_before_eof.py | 4 + 10 files changed, 401 insertions(+), 45 deletions(-) create mode 100644 tests/data/preview/comments9.py create mode 100644 tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py diff --git a/CHANGES.md b/CHANGES.md index ba9f4c06f28..67451f7caf5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ +- Enforce empty lines before classes and functions with sticky leading comments (#3302) + ### Configuration diff --git a/docs/contributing/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst index fa765961e69..3931e0e0072 100644 --- a/docs/contributing/reference/reference_classes.rst +++ b/docs/contributing/reference/reference_classes.rst @@ -11,23 +11,29 @@ .. autoclass:: black.brackets.BracketTracker :members: -:class:`EmptyLineTracker` +:class:`Line` +------------- + +.. autoclass:: black.lines.Line + :members: + :special-members: __str__, __bool__ + +:class:`LinesBlock` ------------------------- -.. autoclass:: black.EmptyLineTracker +.. autoclass:: black.lines.LinesBlock :members: -:class:`Line` -------------- +:class:`EmptyLineTracker` +------------------------- -.. autoclass:: black.Line +.. autoclass:: black.lines.EmptyLineTracker :members: - :special-members: __str__, __bool__ :class:`LineGenerator` ---------------------- -.. autoclass:: black.LineGenerator +.. autoclass:: black.linegen.LineGenerator :show-inheritance: :members: @@ -40,7 +46,7 @@ :class:`Report` --------------- -.. autoclass:: black.Report +.. autoclass:: black.report.Report :members: :special-members: __str__ diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index a028a2888ed..17b7eef092f 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -63,26 +63,47 @@ 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). -### Removing newlines in the beginning of code blocks +### Improved empty line management -_Black_ will remove newlines in the beginning of new code blocks, i.e. when the -indentation level is increased. For example: +1. _Black_ will remove newlines in the beginning of new code blocks, i.e. when the + indentation level is increased. For example: -```python -def my_func(): + ```python + def my_func(): - print("The line above me will be deleted!") -``` + print("The line above me will be deleted!") + ``` -will be changed to: + will be changed to: + + ```python + def my_func(): + print("The line above me will be deleted!") + ``` + + This new feature will be applied to **all code blocks**: `def`, `class`, `if`, + `for`, `while`, `with`, `case` and `match`. + +2. _Black_ will enforce empty lines before classes and functions with leading comments. + For example: + + ```python + some_var = 1 + # Leading sticky comment + def my_func(): + ... + ``` + + will be changed to: + + ```python + some_var = 1 -```python -def my_func(): - print("The line above me will be deleted!") -``` -This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`, -`while`, `with`, `case` and `match`. + # Leading sticky comment + def my_func(): + ... + ``` ### Improved parentheses management diff --git a/src/black/__init__.py b/src/black/__init__.py index 5293796aea1..d9fba41ebd3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -61,7 +61,7 @@ unmask_cell, ) from black.linegen import LN, LineGenerator, transform_line -from black.lines import EmptyLineTracker, Line +from black.lines import EmptyLineTracker, LinesBlock from black.mode import ( FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, @@ -1075,7 +1075,7 @@ def f( def _format_str_once(src_contents: str, *, mode: Mode) -> str: src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) - dst_contents = [] + dst_blocks: List[LinesBlock] = [] if mode.target_versions: versions = mode.target_versions else: @@ -1084,22 +1084,25 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: normalize_fmt_off(src_node, preview=mode.preview) lines = LineGenerator(mode=mode) - elt = EmptyLineTracker(is_pyi=mode.is_pyi) - empty_line = Line(mode=mode) - after = 0 + elt = EmptyLineTracker(mode=mode) split_line_features = { feature for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} if supports_feature(versions, feature) } + block: Optional[LinesBlock] = None for current_line in lines.visit(src_node): - dst_contents.append(str(empty_line) * after) - before, after = elt.maybe_empty_lines(current_line) - dst_contents.append(str(empty_line) * before) + block = elt.maybe_empty_lines(current_line) + dst_blocks.append(block) for line in transform_line( current_line, mode=mode, features=split_line_features ): - dst_contents.append(str(line)) + block.content_lines.append(str(line)) + if dst_blocks: + dst_blocks[-1].after = 0 + dst_contents = [] + for block in dst_blocks: + dst_contents.extend(block.all_lines()) return "".join(dst_contents) diff --git a/src/black/lines.py b/src/black/lines.py index 30622650d53..0d074534def 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -448,6 +448,28 @@ def __bool__(self) -> bool: return bool(self.leaves or self.comments) +@dataclass +class LinesBlock: + """Class that holds information about a block of formatted lines. + + This is introduced so that the EmptyLineTracker can look behind the standalone + comments and adjust their empty lines for class or def lines. + """ + + mode: Mode + previous_block: Optional["LinesBlock"] + original_line: Line + before: int = 0 + content_lines: List[str] = field(default_factory=list) + after: int = 0 + + def all_lines(self) -> List[str]: + empty_line = str(Line(mode=self.mode)) + return ( + [empty_line * self.before] + self.content_lines + [empty_line * self.after] + ) + + @dataclass class EmptyLineTracker: """Provides a stateful method that returns the number of potential extra @@ -458,33 +480,55 @@ class EmptyLineTracker: are consumed by `maybe_empty_lines()` and included in the computation. """ - is_pyi: bool = False + mode: Mode previous_line: Optional[Line] = None - previous_after: int = 0 + previous_block: Optional[LinesBlock] = None previous_defs: List[int] = field(default_factory=list) + semantic_leading_comment: Optional[LinesBlock] = None - def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: + def maybe_empty_lines(self, current_line: Line) -> LinesBlock: """Return the number of extra empty lines before and after the `current_line`. This is for separating `def`, `async def` and `class` with extra empty lines (two on module-level). """ before, after = self._maybe_empty_lines(current_line) + previous_after = self.previous_block.after if self.previous_block else 0 before = ( # Black should not insert empty lines at the beginning # of the file 0 if self.previous_line is None - else before - self.previous_after + else before - previous_after ) - self.previous_after = after + block = LinesBlock( + mode=self.mode, + previous_block=self.previous_block, + original_line=current_line, + before=before, + after=after, + ) + + # Maintain the semantic_leading_comment state. + if current_line.is_comment: + if self.previous_line is None or ( + not self.previous_line.is_decorator + # `or before` means this comment already has an empty line before + and (not self.previous_line.is_comment or before) + and (self.semantic_leading_comment is None or before) + ): + self.semantic_leading_comment = block + elif not current_line.is_decorator: + self.semantic_leading_comment = None + self.previous_line = current_line - return before, after + self.previous_block = block + return block def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: max_allowed = 1 if current_line.depth == 0: - max_allowed = 1 if self.is_pyi else 2 + max_allowed = 1 if self.mode.is_pyi else 2 if current_line.leaves: # Consume the first leaf's extra newlines. first_leaf = current_line.leaves[0] @@ -495,7 +539,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 0 depth = current_line.depth while self.previous_defs and self.previous_defs[-1] >= depth: - if self.is_pyi: + if self.mode.is_pyi: assert self.previous_line is not None if depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. @@ -563,7 +607,7 @@ def _maybe_empty_lines_for_class_or_def( return 0, 0 if self.previous_line.is_decorator: - if self.is_pyi and current_line.is_stub_class: + if self.mode.is_pyi and current_line.is_stub_class: # Insert an empty line after a decorated stub class return 0, 1 @@ -574,14 +618,27 @@ def _maybe_empty_lines_for_class_or_def( ): return 0, 0 + comment_to_add_newlines: Optional[LinesBlock] = None if ( self.previous_line.is_comment and self.previous_line.depth == current_line.depth and before == 0 ): - return 0, 0 + slc = self.semantic_leading_comment + if ( + Preview.empty_lines_before_class_or_def_with_leading_comments + in current_line.mode + and slc is not None + and slc.previous_block is not None + and not slc.previous_block.original_line.is_class + and not slc.previous_block.original_line.opens_block + and slc.before <= 1 + ): + comment_to_add_newlines = slc + else: + return 0, 0 - if self.is_pyi: + if self.mode.is_pyi: if current_line.is_class or self.previous_line.is_class: if self.previous_line.depth < current_line.depth: newlines = 0 @@ -609,6 +666,13 @@ def _maybe_empty_lines_for_class_or_def( newlines = 0 else: newlines = 1 if current_line.depth else 2 + if comment_to_add_newlines is not None: + previous_block = comment_to_add_newlines.previous_block + if previous_block is not None: + comment_to_add_newlines.before = ( + max(comment_to_add_newlines.before, newlines) - previous_block.after + ) + newlines = 0 return newlines, 0 diff --git a/src/black/mode.py b/src/black/mode.py index e3c36450ed1..1e83f2a9c6d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -150,6 +150,7 @@ class Preview(Enum): """Individual preview style features.""" annotation_parens = auto() + empty_lines_before_class_or_def_with_leading_comments = auto() long_docstring_quotes_on_newline = auto() normalize_docstring_quotes_and_prefixes_properly = auto() one_element_subscript = auto() diff --git a/tests/data/preview/comments9.py b/tests/data/preview/comments9.py new file mode 100644 index 00000000000..449612c037a --- /dev/null +++ b/tests/data/preview/comments9.py @@ -0,0 +1,254 @@ +# Test for https://github.com/psf/black/issues/246. + +some = statement +# This comment should be split from the statement above by two lines. +def function(): + pass + + +some = statement +# This multiline comments section +# should be split from the statement +# above by two lines. +def function(): + pass + + +some = statement +# This comment should be split from the statement above by two lines. +async def async_function(): + pass + + +some = statement +# This comment should be split from the statement above by two lines. +class MyClass: + pass + + +some = statement +# This should be stick to the statement above + +# This should be split from the above by two lines +class MyClassWithComplexLeadingComments: + pass + + +class ClassWithDocstring: + """A docstring.""" +# Leading comment after a class with just a docstring +class MyClassAfterAnotherClassWithDocstring: + pass + + +some = statement +# leading 1 +@deco1 +# leading 2 +# leading 2 extra +@deco2(with_args=True) +# leading 3 +@deco3 +# leading 4 +def decorated(): + pass + + +some = statement +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) + +# leading 3 that already has an empty line +@deco3 +# leading 4 +def decorated_with_split_leading_comments(): + pass + + +some = statement +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading 3 +@deco3 + +# leading 4 that already has an empty line +def decorated_with_split_leading_comments(): + pass + + +def main(): + if a: + # Leading comment before inline function + def inline(): + pass + # Another leading comment + def another_inline(): + pass + else: + # More leading comments + def inline_after_else(): + pass + + +if a: + # Leading comment before "top-level inline" function + def top_level_quote_inline(): + pass + # Another leading comment + def another_top_level_quote_inline_inline(): + pass +else: + # More leading comments + def top_level_quote_inline_after_else(): + pass + + +class MyClass: + # First method has no empty lines between bare class def. + # More comments. + def first_method(self): + pass + + +# output + + +# Test for https://github.com/psf/black/issues/246. + +some = statement + + +# This comment should be split from the statement above by two lines. +def function(): + pass + + +some = statement + + +# This multiline comments section +# should be split from the statement +# above by two lines. +def function(): + pass + + +some = statement + + +# This comment should be split from the statement above by two lines. +async def async_function(): + pass + + +some = statement + + +# This comment should be split from the statement above by two lines. +class MyClass: + pass + + +some = statement +# This should be stick to the statement above + + +# This should be split from the above by two lines +class MyClassWithComplexLeadingComments: + pass + + +class ClassWithDocstring: + """A docstring.""" + + +# Leading comment after a class with just a docstring +class MyClassAfterAnotherClassWithDocstring: + pass + + +some = statement + + +# leading 1 +@deco1 +# leading 2 +# leading 2 extra +@deco2(with_args=True) +# leading 3 +@deco3 +# leading 4 +def decorated(): + pass + + +some = statement + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) + +# leading 3 that already has an empty line +@deco3 +# leading 4 +def decorated_with_split_leading_comments(): + pass + + +some = statement + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading 3 +@deco3 + +# leading 4 that already has an empty line +def decorated_with_split_leading_comments(): + pass + + +def main(): + if a: + # Leading comment before inline function + def inline(): + pass + + # Another leading comment + def another_inline(): + pass + + else: + # More leading comments + def inline_after_else(): + pass + + +if a: + # Leading comment before "top-level inline" function + def top_level_quote_inline(): + pass + + # Another leading comment + def another_top_level_quote_inline_inline(): + pass + +else: + # More leading comments + def top_level_quote_inline_after_else(): + pass + + +class MyClass: + # First method has no empty lines between bare class def. + # More comments. + def first_method(self): + pass diff --git a/tests/data/preview/remove_await_parens.py b/tests/data/preview/remove_await_parens.py index eb7dad340c3..571210a2d80 100644 --- a/tests/data/preview/remove_await_parens.py +++ b/tests/data/preview/remove_await_parens.py @@ -80,6 +80,7 @@ async def main(): # output import asyncio + # Control example async def main(): await asyncio.sleep(1) diff --git a/tests/data/simple_cases/comments5.py b/tests/data/simple_cases/comments5.py index d83b6b8ff47..c8c38813d55 100644 --- a/tests/data/simple_cases/comments5.py +++ b/tests/data/simple_cases/comments5.py @@ -58,9 +58,9 @@ def decorated1(): ... -# Note: crappy but inevitable. The current design of EmptyLineTracker doesn't -# allow this to work correctly. The user will have to split those lines by -# hand. +# Note: this is fixed in +# Preview.empty_lines_before_class_or_def_with_leading_comments. +# In the current style, the user will have to split those lines by hand. some_instruction # This comment should be split from `some_instruction` by two lines but isn't. def g(): diff --git a/tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py b/tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py new file mode 100644 index 00000000000..6fea860adf6 --- /dev/null +++ b/tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py @@ -0,0 +1,4 @@ +# Make sure when the file ends with class's docstring, +# It doesn't add extra blank lines. +class ClassWithDocstring: + """A docstring.""" From 527248b3893ea117963ffa74859bb303c0f4c4ba Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Oct 2022 19:03:24 -0700 Subject: [PATCH 346/700] Exclude pytest-xdist 3.0.2 (#3356) We're getting warnings like https://github.com/psf/black/actions/runs/3325521041/jobs/5498291478 and I'm not sure how to fix them. I'll open an issue for a long-term solution, but for now avoid 3.0.2 to unbreak CI. --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index 5bc494d5999..ef61a1210ee 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,6 +1,6 @@ coverage >= 5.3 pre-commit pytest >= 6.1.1 -pytest-xdist >= 2.2.1 +pytest-xdist >= 2.2.1, < 3.0.2 pytest-cov >= 2.11.1 tox From 09d4acdcb79e473276a50c5e5be9dc46fa2634d9 Mon Sep 17 00:00:00 2001 From: Ned Western Date: Wed, 26 Oct 2022 13:04:31 +1100 Subject: [PATCH 347/700] Update README.md (#3284) Minor typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b2593024676..6b72a9b9a23 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Further information can be found in our docs: _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. Now that -we have become stable, you should not expect large formatting to changes in the future. +we have become stable, you should not expect large formatting 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](https://black.readthedocs.io/en/stable/the_black_code_style/index.html). From b73b77a9b039d5afa6ab32202bff2b2232258d45 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 26 Oct 2022 18:03:10 -0700 Subject: [PATCH 348/700] Wrap concatenated strings used as function args in parens (#3307) Fixes #3292 --- CHANGES.md | 2 + src/black/__init__.py | 6 +- src/black/mode.py | 6 +- src/black/parsing.py | 8 +- src/black/trans.py | 43 ++---- tests/data/preview/cantfit.py | 12 +- tests/data/preview/long_strings.py | 54 +++++--- .../data/preview/long_strings__regression.py | 22 +-- tests/test_black.py | 126 ++++++++++++------ 9 files changed, 169 insertions(+), 110 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 67451f7caf5..4db961149bf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Enforce empty lines before classes and functions with sticky leading comments (#3302) +- Implicitly concatenated strings used as function args are now wrapped inside + parentheses (#3307) ### Configuration diff --git a/src/black/__init__.py b/src/black/__init__.py index d9fba41ebd3..94592278c31 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -497,8 +497,10 @@ def main( # noqa: C901 user_level_config = str(find_user_pyproject_toml()) if config == user_level_config: out( - "Using configuration from user-level config at " - f"'{user_level_config}'.", + ( + "Using configuration from user-level config at " + f"'{user_level_config}'." + ), fg="blue", ) elif config_source in ( diff --git a/src/black/mode.py b/src/black/mode.py index 1e83f2a9c6d..e2eff2391b1 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -180,8 +180,10 @@ class Mode: def __post_init__(self) -> None: if self.experimental_string_processing: warn( - "`experimental string processing` has been included in `preview`" - " and deprecated. Use `preview` instead.", + ( + "`experimental string processing` has been included in `preview`" + " and deprecated. Use `preview` instead." + ), Deprecated, ) diff --git a/src/black/parsing.py b/src/black/parsing.py index 762934b8869..c37c12b868d 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -29,9 +29,11 @@ except ImportError: if sys.version_info < (3, 8) and not _IS_PYPY: print( - "The typed_ast package is required but not installed.\n" - "You can upgrade to Python 3.8+ or install typed_ast with\n" - "`python3 -m pip install typed-ast`.", + ( + "The typed_ast package is required but not installed.\n" + "You can upgrade to Python 3.8+ or install typed_ast with\n" + "`python3 -m pip install typed-ast`." + ), file=sys.stderr, ) sys.exit(1) diff --git a/src/black/trans.py b/src/black/trans.py index 7e2d8e67c1a..8893ab02aab 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1058,33 +1058,19 @@ def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]: if LL[0].type != token.STRING: return None - matching_nodes = [ - syms.listmaker, - syms.dictsetmaker, - syms.testlist_gexp, - ] - # If the string is an immediate child of a list/set/tuple literal... - if ( - parent_type(LL[0]) in matching_nodes - or parent_type(LL[0].parent) in matching_nodes + # If the string is surrounded by commas (or is the first/last child)... + prev_sibling = LL[0].prev_sibling + next_sibling = LL[0].next_sibling + if not prev_sibling and not next_sibling and parent_type(LL[0]) == syms.atom: + # If it's an atom string, we need to check the parent atom's siblings. + parent = LL[0].parent + assert parent is not None # For type checkers. + prev_sibling = parent.prev_sibling + next_sibling = parent.next_sibling + if (not prev_sibling or prev_sibling.type == token.COMMA) and ( + not next_sibling or next_sibling.type == token.COMMA ): - # And the string is surrounded by commas (or is the first/last child)... - prev_sibling = LL[0].prev_sibling - next_sibling = LL[0].next_sibling - if ( - not prev_sibling - and not next_sibling - and parent_type(LL[0]) == syms.atom - ): - # If it's an atom string, we need to check the parent atom's siblings. - parent = LL[0].parent - assert parent is not None # For type checkers. - prev_sibling = parent.prev_sibling - next_sibling = parent.next_sibling - if (not prev_sibling or prev_sibling.type == token.COMMA) and ( - not next_sibling or next_sibling.type == token.COMMA - ): - return 0 + return 0 return None @@ -1653,9 +1639,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): assigned the value of some string. OR * The line starts with an "atom" string that prefers to be wrapped in - parens. It's preferred to be wrapped when it's is an immediate child of - a list/set/tuple literal, AND the string is surrounded by commas (or is - the first/last child). + parens. It's preferred to be wrapped when the string is surrounded by + commas (or is the first/last child). Transformations: The chosen string is wrapped in parentheses and then split at the LPAR. diff --git a/tests/data/preview/cantfit.py b/tests/data/preview/cantfit.py index 0849374f776..cade382e30d 100644 --- a/tests/data/preview/cantfit.py +++ b/tests/data/preview/cantfit.py @@ -79,10 +79,14 @@ ) # long arguments normal_name = normal_function_name( - "but with super long string arguments that on their own exceed the line limit so" - " there's no way it can ever fit", - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" - " with spam and eggs and spam with eggs", + ( + "but with super long string arguments that on their own exceed the line limit" + " so there's no way it can ever fit" + ), + ( + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with" + " eggs with spam and eggs and spam with eggs" + ), this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, ) string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 6db3cfed9a9..9288b253b60 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -297,8 +297,10 @@ def foo(): y = "Short string" print( - "This is a really long string inside of a print statement with extra arguments" - " attached at the end of it.", + ( + "This is a really long string inside of a print statement with extra arguments" + " attached at the end of it." + ), x, y, z, @@ -474,13 +476,15 @@ def foo(): ) bad_split_func1( - "But what should happen when code has already " - "been formatted but in the wrong way? Like " - "with a space at the end instead of the " - "beginning. Or what about when it is split too " - "soon? In the case of a split that is too " - "short, black will try to honer the custom " - "split.", + ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), xxx, yyy, zzz, @@ -583,9 +587,11 @@ def foo(): ) arg_comment_string = print( - "Long lines with inline comments which are apart of (and not the only member of) an" - " argument list should have their comments appended to the reformatted string's" - " enclosing left parentheses.", # This comment gets thrown to the top. + ( # This comment gets thrown to the top. + "Long lines with inline comments which are apart of (and not the only member" + " of) an argument list should have their comments appended to the reformatted" + " string's enclosing left parentheses." + ), "Arg #2", "Arg #3", "Arg #4", @@ -645,23 +651,31 @@ def foo(): ) func_with_bad_comma( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), ) func_with_bad_comma( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", # comment after comma + ( # comment after comma + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), ) func_with_bad_comma( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), ) func_with_bad_comma( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", # comment after comma + ( # comment after comma + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), ) func_with_bad_parens_that_wont_fit_in_one_line( diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 634db46a5e0..8b00e76f40e 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -679,9 +679,11 @@ class A: def foo(): some_func_call( "xxxxxxxxxx", - "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " - '"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; ' - "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", + ( + "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " + '"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; ' + "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" " + ), None, ("xxxxxxxxxxx",), ), @@ -690,9 +692,11 @@ def foo(): class A: def foo(): some_func_call( - "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " - "xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; " - "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", + ( + "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " + "xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; " + "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" " + ), None, ("xxxxxxxxxxx",), ), @@ -810,8 +814,10 @@ def foo(): ) lpar_and_rpar_have_comments = func_call( # LPAR Comment - "Long really ridiculous type of string that shouldn't really even exist at all. I" - " mean commmme onnn!!!", # Comma Comment + ( # Comma Comment + "Long really ridiculous type of string that shouldn't really even exist at all." + " I mean commmme onnn!!!" + ), ) # RPAR Comment cmd_fstring = ( diff --git a/tests/test_black.py b/tests/test_black.py index 5d0175d9d66..9256698ea27 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -490,8 +490,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -500,8 +502,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -510,8 +514,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -520,8 +526,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "wat ignored: no match") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -530,22 +538,28 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "f4 already well formatted, good job.") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_report_quiet(self) -> None: @@ -587,8 +601,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -596,8 +612,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 1) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -606,8 +624,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -615,8 +635,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -624,22 +646,28 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_report_normal(self) -> None: @@ -683,8 +711,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -693,8 +723,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -703,8 +735,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -712,8 +746,10 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -721,22 +757,28 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat.", + ( + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat." + ), ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_lib2to3_parse(self) -> None: From d338de7f687e90e292d949d2acd301f59fe5cdf8 Mon Sep 17 00:00:00 2001 From: Hongbo Miao Date: Thu, 27 Oct 2022 14:22:44 -0700 Subject: [PATCH 349/700] Add Archer Aviation to organizations in readme (#3361) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b72a9b9a23..c2dd9e6b17d 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assis Zulip, Kedro, OpenOA, FLORIS, ORBIT, WOMBAT, and many more. The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Mozilla, Quora, -Duolingo, QuantumBlack, Tesla. +Duolingo, QuantumBlack, Tesla, Archer Aviation. Are we missing anyone? Let us know. From 4bb6e4f64ab3820ab9fae6716cd59479d34b7edf Mon Sep 17 00:00:00 2001 From: Corey Hickey Date: Thu, 27 Oct 2022 16:55:33 -0700 Subject: [PATCH 350/700] Vim plugin: allow using system black rather than virtualenv (#3309) Provide a configuration parameter to the Vim plugin which will allow the plugin to skip setting up a virtualenv. This is useful when there is a system installation of black (e.g. from a Linux distribution) which the user prefers to use. Using a virtualenv remains the default. - Fixes #3308 --- CHANGES.md | 3 +++ autoload/black.vim | 10 ++++++++++ docs/integrations/editors.md | 14 +++++++++++++- plugin/black.vim | 3 +++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4db961149bf..1dcd7f09b3c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,9 @@ +- Vim plugin: Optionally allow using the system installation of Black via + `let g:black_use_virtualenv = 0`(#3309) + ### Documentation +- Fix incorrectly ignoring .gitignore presence when more than one source directory is + specified (#3336) + ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 94592278c31..6c8d3468583 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -30,6 +30,7 @@ import click from click.core import ParameterSource from mypy_extensions import mypyc_attr +from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError from _black_version import version as __version__ @@ -627,6 +628,11 @@ def get_sources( sources: Set[Path] = set() root = ctx.obj["root"] + exclude_is_None = exclude is None + exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude + gitignore = None # type: Optional[PathSpec] + root_gitignore = get_gitignore(root) + for s in src: if s == "-" and stdin_filename: p = Path(stdin_filename) @@ -660,16 +666,14 @@ def get_sources( sources.add(p) elif p.is_dir(): - if exclude is None: - exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) - gitignore = get_gitignore(root) + if exclude_is_None: p_gitignore = get_gitignore(p) # No need to use p's gitignore if it is identical to root's gitignore # (i.e. root and p point to the same directory). - if gitignore != p_gitignore: - gitignore += p_gitignore - else: - gitignore = None + if root_gitignore == p_gitignore: + gitignore = root_gitignore + else: + gitignore = root_gitignore + p_gitignore sources.update( gen_python_files( p.iterdir(), diff --git a/tests/data/gitignore_used_on_multiple_sources/.gitignore b/tests/data/gitignore_used_on_multiple_sources/.gitignore new file mode 100644 index 00000000000..2987e7bb646 --- /dev/null +++ b/tests/data/gitignore_used_on_multiple_sources/.gitignore @@ -0,0 +1 @@ +a.py diff --git a/tests/data/gitignore_used_on_multiple_sources/dir1/a.py b/tests/data/gitignore_used_on_multiple_sources/dir1/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/gitignore_used_on_multiple_sources/dir1/b.py b/tests/data/gitignore_used_on_multiple_sources/dir1/b.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/gitignore_used_on_multiple_sources/dir2/a.py b/tests/data/gitignore_used_on_multiple_sources/dir2/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/gitignore_used_on_multiple_sources/dir2/b.py b/tests/data/gitignore_used_on_multiple_sources/dir2/b.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_black.py b/tests/test_black.py index 9256698ea27..784eb0dc9ad 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1998,6 +1998,17 @@ def test_gitignore_used_as_default(self) -> None: ctx.obj["root"] = base assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/") + def test_gitignore_used_on_multiple_sources(self) -> None: + root = Path(DATA_DIR / "gitignore_used_on_multiple_sources") + expected = [ + root / "dir1" / "b.py", + root / "dir2" / "b.py", + ] + ctx = FakeContext() + ctx.obj["root"] = root + src = [root / "dir1", root / "dir2"] + assert_collected_sources(src, expected, ctx=ctx) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_exclude_for_issue_1572(self) -> None: # Exclude shouldn't touch files that were explicitly given to Black through the From ffaaf4838228c922b586a87f717ed402031fcc0a Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Tue, 8 Nov 2022 12:50:04 -0300 Subject: [PATCH 355/700] Compare each .gitignore found with an appropiate relative path (#3338) * Apply .gitignore files considering their location When a .gitignore file contains the special rule to ignore every subfolder content (`*/*`) and the file is located in a subfolder relative to where the command is executed (root), the rule is incorrectly applied and ignores every file at the same level of the .gitignore file. The reason for this is that the `gitignore` variable accumulates the rules found in each .gitignore while traversing files and directories recursively. This makes sense and, in general, works as expected. The problem is that the gitignore rules are applied using as the relative path from root to target directory as a reference. This is the cause of the bug. The implemented solution keeps track of every .gitignore file found while traversing the targets and the absolute location of each .gitignore file. Then, when matching files to the .gitignore rules, compare each set of rules with the appropiate relative path to the candidate target file. To make this possible, we changed the single `gitignore` object with a dictionary of similar objects, where the corresponding key is the absolute path to the folder that contains that .gitignore file. This required changing the signature of the `get_sources` function. Also, we introduce a `is_ignored` function that compares a file with every set of rules. Finally, some tests required an update to pass the gitignore object in the new format. Signed-off-by: Antonio Ossa Guerra * Test .gitignore with `*/*` is applied correctly The test contains three cases: 1) when the .gitignore with the special rule to ignore every subfolder and its contents (*/*) is in the root, 2) when the file is inside a subfolder relative to root (nested), and 3) when the target folder contains the .gitignore and root is a parent folder of the target. In all of these cases, we compare the files that are visible by Black with a known list of paths containing the expected values. Before the fix introduced in the previous commit, these tests failed when the .gitignore file was nested (second case). Now, the test is passed for all cases. Signed-off-by: Antonio Ossa Guerra * Update CHANGES.md Add entry about fixed bug and changes introduced: ignore files by considering the location of each .gitignore file and the relative path of each target Signed-off-by: Antonio Ossa Guerra * Small refactor to improve code readability These changes are small improvements to improve code readability: rename a variable to a more descriptive name (from `exclude_is_None` to `using_default_exclude`), use a better syntax to include the type annotation for `gitignore` variable (from typing comment to Python-style typing annotation), and replace an if-else block with a single dictionary definition (in this case, we need to compare keys instead of values, meaning that the change works) Signed-off-by: Antonio Ossa Guerra * Make nested function a top-level function The function to match a given path with every discovered .gitignore file does not need to be a nested function and can be a top-level function. The arguments did not change, but the naming of local variables was improved for readability. Signed-off-by: Antonio Ossa Guerra Signed-off-by: Antonio Ossa Guerra --- CHANGES.md | 2 ++ src/black/__init__.py | 17 ++++------ src/black/files.py | 28 +++++++++++++--- .../ignore_subfolders_gitignore_tests/a.py | 0 .../subdir/.gitignore | 1 + .../subdir/b.py | 0 .../subdir/subdir/c.py | 0 tests/test_black.py | 32 +++++++++++++++++-- 8 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 tests/data/ignore_subfolders_gitignore_tests/a.py create mode 100644 tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore create mode 100644 tests/data/ignore_subfolders_gitignore_tests/subdir/b.py create mode 100644 tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py diff --git a/CHANGES.md b/CHANGES.md index a1071a8ec7c..a9a1b279ddc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- Fix incorrectly applied .gitignore rules by considering the .gitignore location and + the relative path to the target file (#3338) - Fix incorrectly ignoring .gitignore presence when more than one source directory is specified (#3336) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6c8d3468583..2786861e9e0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -628,9 +628,9 @@ def get_sources( sources: Set[Path] = set() root = ctx.obj["root"] - exclude_is_None = exclude is None + using_default_exclude = exclude is None exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude - gitignore = None # type: Optional[PathSpec] + gitignore: Optional[PathSpec] = None root_gitignore = get_gitignore(root) for s in src: @@ -666,14 +666,11 @@ def get_sources( sources.add(p) elif p.is_dir(): - if exclude_is_None: - p_gitignore = get_gitignore(p) - # No need to use p's gitignore if it is identical to root's gitignore - # (i.e. root and p point to the same directory). - if root_gitignore == p_gitignore: - gitignore = root_gitignore - else: - gitignore = root_gitignore + p_gitignore + if using_default_exclude: + gitignore = { + root: root_gitignore, + root / p: get_gitignore(p), + } sources.update( gen_python_files( p.iterdir(), diff --git a/src/black/files.py b/src/black/files.py index ed503f5fec7..ea517f4ece9 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -182,6 +182,19 @@ def normalize_path_maybe_ignore( return root_relative_path +def path_is_ignored( + path: Path, gitignore_dict: Dict[Path, PathSpec], report: Report +) -> bool: + for gitignore_path, pattern in gitignore_dict.items(): + relative_path = normalize_path_maybe_ignore(path, gitignore_path, report) + if relative_path is None: + break + if pattern.match_file(relative_path): + report.path_ignored(path, "matches a .gitignore file content") + return True + return False + + def path_is_excluded( normalized_path: str, pattern: Optional[Pattern[str]], @@ -198,7 +211,7 @@ def gen_python_files( extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: Report, - gitignore: Optional[PathSpec], + gitignore_dict: Optional[Dict[Path, PathSpec]], *, verbose: bool, quiet: bool, @@ -211,6 +224,7 @@ def gen_python_files( `report` is where output about exclusions goes. """ + assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: normalized_path = normalize_path_maybe_ignore(child, root, report) @@ -218,8 +232,7 @@ def gen_python_files( continue # First ignore files matching .gitignore, if passed - if gitignore is not None and gitignore.match_file(normalized_path): - report.path_ignored(child, "matches the .gitignore file content") + if gitignore_dict and path_is_ignored(child, gitignore_dict, report): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. @@ -244,6 +257,13 @@ def gen_python_files( if child.is_dir(): # If gitignore is None, gitignore usage is disabled, while a Falsey # gitignore is when the directory doesn't have a .gitignore file. + if gitignore_dict is not None: + new_gitignore_dict = { + **gitignore_dict, + root / child: get_gitignore(child), + } + else: + new_gitignore_dict = None yield from gen_python_files( child.iterdir(), root, @@ -252,7 +272,7 @@ def gen_python_files( extend_exclude, force_exclude, report, - gitignore + get_gitignore(child) if gitignore is not None else None, + new_gitignore_dict, verbose=verbose, quiet=quiet, ) diff --git a/tests/data/ignore_subfolders_gitignore_tests/a.py b/tests/data/ignore_subfolders_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore b/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore new file mode 100644 index 00000000000..150f68c80f5 --- /dev/null +++ b/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore @@ -0,0 +1 @@ +*/* diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py b/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py b/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_black.py b/tests/test_black.py index 784eb0dc9ad..a43f05e083b 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2042,7 +2042,7 @@ def test_gitignore_exclude(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, ) @@ -2071,7 +2071,7 @@ def test_nested_gitignore(self) -> None: None, None, report, - root_gitignore, + {path: root_gitignore}, verbose=False, quiet=False, ) @@ -2109,6 +2109,32 @@ def test_invalid_nested_gitignore(self) -> None: gitignore = path / "a" / ".gitignore" assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + def test_gitignore_that_ignores_subfolders(self) -> None: + # If gitignore with */* is in root + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir") + expected = [root / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If .gitignore with */* is nested + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + expected = [ + root / "a.py", + root / "subdir" / "b.py", + ] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If command is executed from outer dir + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + target = root / "subdir" + expected = [target / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([target], expected, ctx=ctx) + def test_empty_include(self) -> None: path = DATA_DIR / "include_exclude_tests" src = [path] @@ -2163,7 +2189,7 @@ def test_symlink_out_of_root_directory(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, ) From 8091b2503d12dd0110e0f1a9ed1e6b830aa668ed Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 9 Nov 2022 15:08:51 -0800 Subject: [PATCH 356/700] Correctly handle trailing commas that are inside a line's leading non-nested parens (#3370) - Fixes #1671 - Fixes #3229 --- CHANGES.md | 2 + src/black/brackets.py | 34 ++++++++- src/black/linegen.py | 70 ++++++++++++++---- src/black/lines.py | 6 +- src/black/mode.py | 1 + .../data/preview/skip_magic_trailing_comma.py | 40 ++++++++++ .../trailing_commas_in_leading_parts.py | 74 +++++++++++++++++++ .../simple_cases/function_trailing_comma.py | 29 ++++++++ 8 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 tests/data/preview/trailing_commas_in_leading_parts.py diff --git a/CHANGES.md b/CHANGES.md index a9a1b279ddc..4d1887f2842 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Enforce empty lines before classes and functions with sticky leading comments (#3302) - Implicitly concatenated strings used as function args are now wrapped inside parentheses (#3307) +- Correctly handle trailing commas that are inside a line's leading non-nested parens + (#3370) ### Configuration diff --git a/src/black/brackets.py b/src/black/brackets.py index 3566f5b6c37..0a5317f6773 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -2,7 +2,7 @@ import sys from dataclasses import dataclass, field -from typing import Dict, Iterable, List, Optional, Tuple, Union +from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union if sys.version_info < (3, 8): from typing_extensions import Final @@ -340,3 +340,35 @@ def max_delimiter_priority_in_atom(node: LN) -> Priority: except ValueError: return 0 + + +def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]: + """Return leaves that are inside matching brackets. + + The input `leaves` can have non-matching brackets at the head or tail parts. + Matching brackets are included. + """ + try: + # Only track brackets from the first opening bracket to the last closing + # bracket. + start_index = next( + i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS + ) + end_index = next( + len(leaves) - i + for i, l in enumerate(reversed(leaves)) + if l.type in CLOSING_BRACKETS + ) + except StopIteration: + return set() + ids = set() + depth = 0 + for i in range(end_index, start_index - 1, -1): + leaf = leaves[i] + if leaf.type in CLOSING_BRACKETS: + depth += 1 + if depth > 0: + ids.add(id(leaf)) + if leaf.type in OPENING_BRACKETS: + depth -= 1 + return ids diff --git a/src/black/linegen.py b/src/black/linegen.py index a2e41bf5912..219495e9a5e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,10 +2,16 @@ Generating lines of code. """ import sys +from enum import Enum, auto from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast -from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, max_delimiter_priority_in_atom +from black.brackets import ( + COMMA_PRIORITY, + DOT_PRIORITY, + get_leaves_inside_matching_brackets, + max_delimiter_priority_in_atom, +) from black.comments import FMT_OFF, generate_comments, list_comments from black.lines import ( Line, @@ -561,6 +567,12 @@ def _rhs( yield line +class _BracketSplitComponent(Enum): + head = auto() + body = auto() + tail = auto() + + def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator[Line]: """Split line into many lines, starting with the first matching bracket pair. @@ -591,9 +603,15 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator if not matching_bracket: raise CannotSplit("No brackets found") - head = bracket_split_build_line(head_leaves, line, matching_bracket) - body = bracket_split_build_line(body_leaves, line, matching_bracket, is_body=True) - tail = bracket_split_build_line(tail_leaves, line, matching_bracket) + head = bracket_split_build_line( + head_leaves, line, matching_bracket, component=_BracketSplitComponent.head + ) + body = bracket_split_build_line( + body_leaves, line, matching_bracket, component=_BracketSplitComponent.body + ) + tail = bracket_split_build_line( + tail_leaves, line, matching_bracket, component=_BracketSplitComponent.tail + ) bracket_split_succeeded_or_raise(head, body, tail) for result in (head, body, tail): if result: @@ -639,9 +657,15 @@ def right_hand_split( tail_leaves.reverse() body_leaves.reverse() head_leaves.reverse() - head = bracket_split_build_line(head_leaves, line, opening_bracket) - body = bracket_split_build_line(body_leaves, line, opening_bracket, is_body=True) - tail = bracket_split_build_line(tail_leaves, line, opening_bracket) + head = bracket_split_build_line( + head_leaves, line, opening_bracket, component=_BracketSplitComponent.head + ) + body = bracket_split_build_line( + body_leaves, line, opening_bracket, component=_BracketSplitComponent.body + ) + tail = bracket_split_build_line( + tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail + ) bracket_split_succeeded_or_raise(head, body, tail) if ( Feature.FORCE_OPTIONAL_PARENTHESES not in features @@ -715,15 +739,23 @@ def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None def bracket_split_build_line( - leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False + leaves: List[Leaf], + original: Line, + opening_bracket: Leaf, + *, + component: _BracketSplitComponent, ) -> Line: """Return a new line with given `leaves` and respective comments from `original`. - If `is_body` is True, the result line is one-indented inside brackets and as such - has its first leaf's prefix normalized and a trailing comma added when expected. + If it's the head component, brackets will be tracked so trailing commas are + respected. + + If it's the body component, the result line is one-indented inside brackets and as + such has its first leaf's prefix normalized and a trailing comma added when + expected. """ result = Line(mode=original.mode, depth=original.depth) - if is_body: + if component is _BracketSplitComponent.body: result.inside_brackets = True result.depth += 1 if leaves: @@ -761,12 +793,24 @@ def bracket_split_build_line( leaves.insert(i + 1, new_comma) break + leaves_to_track: Set[LeafID] = set() + if ( + Preview.handle_trailing_commas_in_head in original.mode + and component is _BracketSplitComponent.head + ): + leaves_to_track = get_leaves_inside_matching_brackets(leaves) # Populate the line for leaf in leaves: - result.append(leaf, preformatted=True) + result.append( + leaf, + preformatted=True, + track_bracket=id(leaf) in leaves_to_track, + ) for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) - if is_body and should_split_line(result, opening_bracket): + if component is _BracketSplitComponent.body and should_split_line( + result, opening_bracket + ): result.should_split_rhs = True return result diff --git a/src/black/lines.py b/src/black/lines.py index 0d074534def..08281bcf370 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -53,7 +53,9 @@ class Line: should_split_rhs: bool = False magic_trailing_comma: Optional[Leaf] = None - def append(self, leaf: Leaf, preformatted: bool = False) -> None: + def append( + self, leaf: Leaf, preformatted: bool = False, track_bracket: bool = False + ) -> None: """Add a new `leaf` to the end of the line. Unless `preformatted` is True, the `leaf` will receive a new consistent @@ -75,7 +77,7 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: leaf.prefix += whitespace( leaf, complex_subscript=self.is_complex_subscript(leaf) ) - if self.inside_brackets or not preformatted: + if self.inside_brackets or not preformatted or track_bracket: self.bracket_tracker.mark(leaf) if self.mode.magic_trailing_comma: if self.has_magic_trailing_comma(leaf): diff --git a/src/black/mode.py b/src/black/mode.py index e2eff2391b1..a3ce20b8619 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -151,6 +151,7 @@ class Preview(Enum): annotation_parens = auto() empty_lines_before_class_or_def_with_leading_comments = auto() + handle_trailing_commas_in_head = auto() long_docstring_quotes_on_newline = auto() normalize_docstring_quotes_and_prefixes_properly = auto() one_element_subscript = auto() diff --git a/tests/data/preview/skip_magic_trailing_comma.py b/tests/data/preview/skip_magic_trailing_comma.py index e98174af427..c020db79864 100644 --- a/tests/data/preview/skip_magic_trailing_comma.py +++ b/tests/data/preview/skip_magic_trailing_comma.py @@ -15,6 +15,37 @@ # Except single element tuples small_tuple = (1,) +# Trailing commas in multiple chained non-nested parens. +zero( + one, +).two( + three, +).four( + five, +) + +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + +( + a, + b, + c, + d, +) = func1( + arg1 +) and func2(arg2) + +func( + argument1, + ( + one, + two, + ), + argument4, + argument5, + argument6, +) + # output # We should not remove the trailing comma in a single-element subscript. a: tuple[int,] @@ -32,3 +63,12 @@ # Except single element tuples small_tuple = (1,) + +# Trailing commas in multiple chained non-nested parens. +zero(one).two(three).four(five) + +func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) + +(a, b, c, d) = func1(arg1) and func2(arg2) + +func(argument1, (one, two), argument4, argument5, argument6) diff --git a/tests/data/preview/trailing_commas_in_leading_parts.py b/tests/data/preview/trailing_commas_in_leading_parts.py new file mode 100644 index 00000000000..676725c12a3 --- /dev/null +++ b/tests/data/preview/trailing_commas_in_leading_parts.py @@ -0,0 +1,74 @@ +zero(one,).two(three,).four(five,) + +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + +# Inner one-element tuple shouldn't explode +func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) + +(a, b, c, d,) = func1(arg1) and func2(arg2) + + +# Example from https://github.com/psf/black/issues/3229 +def refresh_token(self, device_family, refresh_token, api_key): + return self.orchestration.refresh_token( + data={ + "refreshToken": refresh_token, + }, + api_key=api_key, + )["extensions"]["sdk"]["token"] + + +# Edge case where a bug in a working-in-progress version of +# https://github.com/psf/black/pull/3370 causes an infinite recursion. +assert ( + long_module.long_class.long_func().another_func() + == long_module.long_class.long_func()["some_key"].another_func(arg1) +) + + +# output + + +zero( + one, +).two( + three, +).four( + five, +) + +func1(arg1).func2( + arg2, +).func3(arg3).func4( + arg4, +).func5(arg5) + +# Inner one-element tuple shouldn't explode +func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) + +( + a, + b, + c, + d, +) = func1( + arg1 +) and func2(arg2) + + +# Example from https://github.com/psf/black/issues/3229 +def refresh_token(self, device_family, refresh_token, api_key): + return self.orchestration.refresh_token( + data={ + "refreshToken": refresh_token, + }, + api_key=api_key, + )["extensions"]["sdk"]["token"] + + +# Edge case where a bug in a working-in-progress version of +# https://github.com/psf/black/pull/3370 causes an infinite recursion. +assert ( + long_module.long_class.long_func().another_func() + == long_module.long_class.long_func()["some_key"].another_func(arg1) +) diff --git a/tests/data/simple_cases/function_trailing_comma.py b/tests/data/simple_cases/function_trailing_comma.py index 429eb0e330f..abe9617c0e9 100644 --- a/tests/data/simple_cases/function_trailing_comma.py +++ b/tests/data/simple_cases/function_trailing_comma.py @@ -49,6 +49,17 @@ def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_cr ): pass + +# Make sure inner one-element tuple won't explode +some_module.some_function( + argument1, (one_element_tuple,), argument4, argument5, argument6 +) + +# Inner trailing comma causes outer to explode +some_module.some_function( + argument1, (one, two,), argument4, argument5, argument6 +) + # output def f( @@ -151,3 +162,21 @@ def func() -> ( ) ): pass + + +# Make sure inner one-element tuple won't explode +some_module.some_function( + argument1, (one_element_tuple,), argument4, argument5, argument6 +) + +# Inner trailing comma causes outer to explode +some_module.some_function( + argument1, + ( + one, + two, + ), + argument4, + argument5, + argument6, +) From c23a5c15aac963b460fdb1327ae875aafafafd41 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:03:19 +0100 Subject: [PATCH 357/700] Clarify that Black runs with --safe by default (#3378) --- docs/the_black_code_style/current_style.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 59d79c4cd0e..0f166fe2bd4 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -457,9 +457,9 @@ the latter are treated as true raw strings with no special semantics. ### AST before and after formatting -When run with `--safe`, _Black_ checks that the code before and after is semantically -equivalent. This check is done by comparing the AST of the source with the AST of the -target. There are three limited cases in which the AST does differ: +When run with `--safe` (the default), _Black_ checks that the code before and after is +semantically equivalent. This check is done by comparing the AST of the source with the +AST of the target. There are three limited cases in which the AST does differ: 1. _Black_ cleans up leading and trailing whitespace of docstrings, re-indenting them if needed. It's been one of the most popular user-reported features for the formatter to From d97b7898b34b67eb3c6839998920e17ac8c77908 Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Fri, 11 Nov 2022 22:05:36 -0300 Subject: [PATCH 358/700] Remove whitespaces of whitespace-only files (#3348) Currently, empty and whitespace-only (with or without newlines) are not modified. In some discussions (issues and pull requests) consensus was to reformat whitespace-only files to empty or single-character files, preserving line endings when possible. With that said, this commit introduces the following behaviors: * Empty files are left as is * Whitespace-only files (no newline) reformat into empty files * Whitespace-only files (1 or more newlines) reformat into a single newline character To implement these changes, we moved the initial check at `format_file_contents` that raises `NothingChanged` if the source (with no whitespaces) is an empty string. In the case of *.ipynb files, `format_ipynb_string` checks a similar condition and removed whitespaces. In the case of Python files, `format_str_once` includes a check on the output that returns the correct newline character if possible or an empty string otherwise. Signed-off-by: Antonio Ossa Guerra --- CHANGES.md | 2 + src/black/__init__.py | 12 ++++- tests/data/preview/whitespace.py | 6 +++ tests/test_black.py | 87 +++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/data/preview/whitespace.py diff --git a/CHANGES.md b/CHANGES.md index 4d1887f2842..992b3800405 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Enforce empty lines before classes and functions with sticky leading comments (#3302) +- Reformat empty and whitespace-only files as either an empty file (if no newline is + present) or as a single newline character (if a newline is present) (#3348) - Implicitly concatenated strings used as function args are now wrapped inside parentheses (#3307) - Correctly handle trailing commas that are inside a line's leading non-nested parens diff --git a/src/black/__init__.py b/src/black/__init__.py index 2786861e9e0..7d7ddbe0930 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -917,7 +917,7 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. `mode` is passed to :func:`format_str`. """ - if not src_contents.strip(): + if not mode.preview and not src_contents.strip(): raise NothingChanged if mode.is_ipynb: @@ -1014,6 +1014,9 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon Operate cell-by-cell, only on code cells, only for Python notebooks. If the ``.ipynb`` originally had a trailing newline, it'll be preserved. """ + if mode.preview and not src_contents: + raise NothingChanged + trailing_newline = src_contents[-1] == "\n" modified = False nb = json.loads(src_contents) @@ -1106,6 +1109,13 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: dst_contents = [] for block in dst_blocks: dst_contents.extend(block.all_lines()) + if mode.preview and not dst_contents: + # Use decode_bytes to retrieve the correct source newline (CRLF or LF), + # and check if normalized_content has more than one line + normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8")) + if "\n" in normalized_content: + return newline + return "" return "".join(dst_contents) diff --git a/tests/data/preview/whitespace.py b/tests/data/preview/whitespace.py new file mode 100644 index 00000000000..a319c0117b1 --- /dev/null +++ b/tests/data/preview/whitespace.py @@ -0,0 +1,6 @@ + + + + + +# output diff --git a/tests/test_black.py b/tests/test_black.py index a43f05e083b..dda10555c97 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -25,6 +25,7 @@ List, Optional, Sequence, + Type, TypeVar, Union, ) @@ -153,6 +154,34 @@ def test_empty_ff(self) -> None: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) + @patch("black.dump_to_file", dump_to_stderr) + def test_one_empty_line(self) -> None: + mode = black.Mode(preview=True) + for nl in ["\n", "\r\n"]: + source = expected = nl + assert_format(source, expected, mode=mode) + + def test_one_empty_line_ff(self) -> None: + mode = black.Mode(preview=True) + for nl in ["\n", "\r\n"]: + expected = nl + tmp_file = Path(black.dump_to_file(nl)) + if system() == "Windows": + # Writing files in text mode automatically uses the system newline, + # but in this case we don't want this for testing reasons. See: + # https://github.com/psf/black/pull/3348 + with open(tmp_file, "wb") as f: + f.write(nl.encode("utf-8")) + try: + self.assertFalse( + ff(tmp_file, mode=mode, write_back=black.WriteBack.YES) + ) + with open(tmp_file, "rb") as f: + actual = f.read().decode("utf8") + finally: + 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 @@ -971,8 +1000,8 @@ def err(msg: str, **kwargs: Any) -> None: ) def test_format_file_contents(self) -> None: - empty = "" mode = DEFAULT_MODE + empty = "" with self.assertRaises(black.NothingChanged): black.format_file_contents(empty, mode=mode, fast=False) just_nl = "\n" @@ -990,6 +1019,17 @@ def test_format_file_contents(self) -> None: black.format_file_contents(invalid, mode=mode, fast=False) self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can") + mode = black.Mode(preview=True) + just_crlf = "\r\n" + with self.assertRaises(black.NothingChanged): + black.format_file_contents(just_crlf, mode=mode, fast=False) + just_whitespace_nl = "\n\t\n \n\t \n \t\n\n" + actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False) + self.assertEqual("\n", actual) + just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n" + actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False) + self.assertEqual("\r\n", actual) + def test_endmarker(self) -> None: n = black.lib2to3_parse("\n") self.assertEqual(n.type, black.syms.file_input) @@ -1281,8 +1321,51 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None: report.done.assert_called_with(expected, black.Changed.YES) def test_reformat_one_with_stdin_empty(self) -> None: + cases = [ + ("", ""), + ("\n", "\n"), + ("\r\n", "\r\n"), + (" \t", ""), + (" \t\n\t ", "\n"), + (" \t\r\n\t ", "\r\n"), + ] + + def _new_wrapper( + output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper] + ) -> Callable[[Any, Any], io.TextIOWrapper]: + def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: + if args == (sys.stdout.buffer,): + # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`, + # return our mock object. + return output + # It's something else (i.e. `decode_bytes()`) calling + # `io.TextIOWrapper()`, pass through to the original implementation. + # See discussion in https://github.com/psf/black/pull/2489 + return io_TextIOWrapper(*args, **kwargs) + + return get_output + + mode = black.Mode(preview=True) + for content, expected in cases: + output = io.StringIO() + io_TextIOWrapper = io.TextIOWrapper + + with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): + try: + black.format_stdin_to_stdout( + fast=True, + content=content, + write_back=black.WriteBack.YES, + mode=mode, + ) + except io.UnsupportedOperation: + pass # StringIO does not support detach + assert output.getvalue() == expected + + # An empty string is the only test case for `preview=False` output = io.StringIO() - with patch("io.TextIOWrapper", lambda *args, **kwargs: output): + io_TextIOWrapper = io.TextIOWrapper + with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): try: black.format_stdin_to_stdout( fast=True, From 27932494bcefac03497dd92dcf0c59a04c10d757 Mon Sep 17 00:00:00 2001 From: sckarlin Date: Mon, 14 Nov 2022 10:31:43 -0500 Subject: [PATCH 359/700] Wordsmith current_style.md (#3383) "realtime" doesn't make sense in this context. --- docs/the_black_code_style/current_style.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 0f166fe2bd4..56b92529d70 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -464,8 +464,8 @@ AST of the target. There are three limited cases in which the AST does differ: 1. _Black_ cleans up leading and trailing whitespace of docstrings, re-indenting them if needed. It's been one of the most popular user-reported features for the formatter to fix whitespace issues with docstrings. While the result is technically an AST - difference, due to the various possibilities of forming docstrings, all realtime use - of docstrings that we're aware of sanitizes indentation and leading/trailing + difference, due to the various possibilities of forming docstrings, all real-world + uses of docstrings that we're aware of sanitize indentation and leading/trailing whitespace anyway. 1. _Black_ manages optional parentheses for some statements. In the case of the `del` From d4a85643a465f5fae2113d07d22d021d4af4795a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 08:24:46 -0800 Subject: [PATCH 360/700] Bump sphinx-copybutton from 0.5.0 to 0.5.1 in /docs (#3390) Bumps [sphinx-copybutton](https://github.com/executablebooks/sphinx-copybutton) from 0.5.0 to 0.5.1. - [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.5.0...v0.5.1) --- updated-dependencies: - dependency-name: sphinx-copybutton dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 c2342a35d7b..426a78a7abf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,5 +5,5 @@ Sphinx==5.3.0 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 -sphinx_copybutton==0.5.0 +sphinx_copybutton==0.5.1 furo==2022.9.29 From 19c5fe429e355a53ff320065d22e661785040b4c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Dec 2022 20:11:07 -0800 Subject: [PATCH 361/700] Fix CI with latest flake8-bugbear (#3412) --- .flake8 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index ae11a13347c..eddaaba81fd 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,6 @@ [flake8] -ignore = E203, E266, E501, W503 +# B905 should be enabled when we drop support for 3.9 +ignore = E203, E266, E501, W503, B905 # line length is intentionally set to 80 here because black uses Bugbear # See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details max-line-length = 80 From 9ace064d85e1fb0497db635e485fdab1e174e737 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:28:29 -0500 Subject: [PATCH 362/700] Bump peter-evans/find-comment from 2.0.1 to 2.1.0 (#3404) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 1d35432a295..1375be9788d 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea + uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From 5b1443aefd9ad2bbaa20a76997183225dbfcc0fb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Dec 2022 20:36:39 -0800 Subject: [PATCH 363/700] release: skip bad macos wheels for now (#3411) Workaround for #3312 --- .github/workflows/pypi_upload.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index a5b4af37915..16cf90bd206 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -47,12 +47,13 @@ jobs: - os: macos-11 name: macos-x86_64 macos_arch: "x86_64" - - os: macos-11 - name: macos-arm64 - macos_arch: "arm64" - - os: macos-11 - name: macos-universal2 - macos_arch: "universal2" + # Only build x86_64 wheels on macos until #3312 is fixed + # - os: macos-11 + # name: macos-arm64 + # macos_arch: "arm64" + # - os: macos-11 + # name: macos-universal2 + # macos_arch: "universal2" steps: - uses: actions/checkout@v3 From 2ddea293a88919650266472186620a98a4a8bb37 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Dec 2022 07:49:43 -0800 Subject: [PATCH 364/700] Prepare release 22.12.0 (#3413) --- CHANGES.md | 58 ++++++++++++++------- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 992b3800405..20ae65d36f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,23 +14,10 @@ -- Enforce empty lines before classes and functions with sticky leading comments (#3302) -- Reformat empty and whitespace-only files as either an empty file (if no newline is - present) or as a single newline character (if a newline is present) (#3348) -- Implicitly concatenated strings used as function args are now wrapped inside - parentheses (#3307) -- Correctly handle trailing commas that are inside a line's leading non-nested parens - (#3370) - ### Configuration -- Fix incorrectly applied .gitignore rules by considering the .gitignore location and - the relative path to the target file (#3338) -- Fix incorrectly ignoring .gitignore presence when more than one source directory is - specified (#3336) - ### Packaging @@ -39,10 +26,6 @@ -- Parsing support has been added for walruses inside generator expression that are - passed as function args (for example, - `any(match := my_re.match(text) for text in texts)`) (#3327). - ### Performance @@ -59,14 +42,49 @@ -- Vim plugin: Optionally allow using the system installation of Black via - `let g:black_use_virtualenv = 0`(#3309) - ### Documentation +## 22.12.0 + +### Preview style + + + +- Enforce empty lines before classes and functions with sticky leading comments (#3302) +- Reformat empty and whitespace-only files as either an empty file (if no newline is + present) or as a single newline character (if a newline is present) (#3348) +- Implicitly concatenated strings used as function args are now wrapped inside + parentheses (#3307) +- Correctly handle trailing commas that are inside a line's leading non-nested parens + (#3370) + +### Configuration + + + +- Fix incorrectly applied `.gitignore` rules by considering the `.gitignore` location + and the relative path to the target file (#3338) +- Fix incorrectly ignoring `.gitignore` presence when more than one source directory is + specified (#3336) + +### Parser + + + +- Parsing support has been added for walruses inside generator expression that are + passed as function args (for example, + `any(match := my_re.match(text) for text in texts)`) (#3327). + +### Integrations + + + +- Vim plugin: Optionally allow using the system installation of Black via + `let g:black_use_virtualenv = 0`(#3309) + ## 22.10.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 4189b5c4b06..712b9a688d1 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: 22.10.0 + rev: 22.12.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 80897532a68..3dab644f2c8 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -175,7 +175,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, version 22.10.0 +black, version 22.12.0 ``` An option to require a specific version to be running is also provided. From 1f7f6de4aba4e1e42cb2f947204f8256f7370cb0 Mon Sep 17 00:00:00 2001 From: Isac Byeonghoon Yoo Date: Sun, 11 Dec 2022 00:08:05 +0900 Subject: [PATCH 365/700] Fix type annotation for gitignore pathspec (#3416) --- 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 7d7ddbe0930..39d12968c4b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -630,7 +630,7 @@ def get_sources( using_default_exclude = exclude is None exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude - gitignore: Optional[PathSpec] = None + gitignore: Optional[Dict[Path, PathSpec]] = None root_gitignore = get_gitignore(root) for s in src: From 96e62c57e3023977de177a8ba34678007a63f1fe Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sat, 10 Dec 2022 07:58:45 -0800 Subject: [PATCH 366/700] Fix a crash in preview style with assert + parenthesized string. (#3415) The bug is in the `get_leaves_inside_matching_brackets` on the third line below: ```python assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( xxxxxxxxx ).xxxxxxxxxxxxxxxxxx(), ( "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ) ``` Including the invisible paren, third line is `).xxxxxxxxxxxxxxxxxx()), (`, that it has a matched pair then an unmatched closing paren afterwards. This PR ensures the returned leaves are actually matched. Fixes #3414. --- CHANGES.md | 2 ++ src/black/brackets.py | 25 ++++++++----------- .../trailing_commas_in_leading_parts.py | 14 +++++++++++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 20ae65d36f3..c84feb04934 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ +- Fix a crash in preview style with assert + parenthesized string (#3415) + ### Configuration diff --git a/src/black/brackets.py b/src/black/brackets.py index 0a5317f6773..ec9708cb08a 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -349,26 +349,23 @@ def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]: Matching brackets are included. """ try: - # Only track brackets from the first opening bracket to the last closing - # bracket. + # Start with the first opening bracket and ignore closing brackets before. start_index = next( i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS ) - end_index = next( - len(leaves) - i - for i, l in enumerate(reversed(leaves)) - if l.type in CLOSING_BRACKETS - ) except StopIteration: return set() + bracket_stack = [] ids = set() - depth = 0 - for i in range(end_index, start_index - 1, -1): + for i in range(start_index, len(leaves)): leaf = leaves[i] - if leaf.type in CLOSING_BRACKETS: - depth += 1 - if depth > 0: - ids.add(id(leaf)) if leaf.type in OPENING_BRACKETS: - depth -= 1 + bracket_stack.append((BRACKET[leaf.type], i)) + if leaf.type in CLOSING_BRACKETS: + if bracket_stack and leaf.type == bracket_stack[-1][0]: + _, start = bracket_stack.pop() + for j in range(start, i + 1): + ids.add(id(leaves[j])) + else: + break return ids diff --git a/tests/data/preview/trailing_commas_in_leading_parts.py b/tests/data/preview/trailing_commas_in_leading_parts.py index 676725c12a3..99d82a677f8 100644 --- a/tests/data/preview/trailing_commas_in_leading_parts.py +++ b/tests/data/preview/trailing_commas_in_leading_parts.py @@ -25,6 +25,13 @@ def refresh_token(self, device_family, refresh_token, api_key): == long_module.long_class.long_func()["some_key"].another_func(arg1) ) +# Regression test for https://github.com/psf/black/issues/3414. +assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( + xxxxxxxxx +).xxxxxxxxxxxxxxxxxx(), ( + "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +) + # output @@ -72,3 +79,10 @@ def refresh_token(self, device_family, refresh_token, api_key): long_module.long_class.long_func().another_func() == long_module.long_class.long_func()["some_key"].another_func(arg1) ) + +# Regression test for https://github.com/psf/black/issues/3414. +assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( + xxxxxxxxx +).xxxxxxxxxxxxxxxxxx(), ( + "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +) From 16b98abca94343770aad561ea659380c97d473b4 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Sat, 10 Dec 2022 19:49:33 +0000 Subject: [PATCH 367/700] make black[jupyter] installation cross-shell (#3394) --- README.md | 2 +- docs/getting_started.md | 2 +- src/black/handle_ipynb_magics.py | 2 +- tests/test_no_ipynb.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c2dd9e6b17d..b12ddfb1290 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation _Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. -If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. +If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/getting_started.md b/docs/getting_started.md index 1825f3b5aa3..33fb2f978bb 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -17,7 +17,7 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation _Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. -If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`. +If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 693f1a68bd4..9e1af757c32 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -64,7 +64,7 @@ def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: if verbose or not quiet: msg = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``" + 'You can fix this by running ``pip install "black[jupyter]"``' ) out(msg) return False diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index 3e0b1593bf0..b63ecde8896 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -17,7 +17,7 @@ def test_ipynb_diff_with_no_change_single() -> None: result = runner.invoke(main, [str(path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``\n" + 'You can fix this by running ``pip install "black[jupyter]"``\n' ) assert expected_output in result.output @@ -32,6 +32,6 @@ def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None: result = runner.invoke(main, [str(tmp_path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - "You can fix this by running ``pip install black[jupyter]``\n" + 'You can fix this by running ``pip install "black[jupyter]"``\n' ) assert expected_output in result.output From 5f0dc862f5bbfc8abed3f29d76325404eb4f99c0 Mon Sep 17 00:00:00 2001 From: mainj12 <118842653+mainj12@users.noreply.github.com> Date: Sat, 10 Dec 2022 20:56:14 +0000 Subject: [PATCH 368/700] Adding pyproject.toml configuration output to verbose logging (#3392) --- CHANGES.md | 3 +++ src/black/__init__.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c84feb04934..e781cbd523e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,9 @@ +- Verbose logging now shows the values of `pyproject.toml` configuration variables + (#3392) + ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 39d12968c4b..f00749aaed8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -511,6 +511,9 @@ def main( # noqa: C901 out("Using configuration from project root.", fg="blue") else: out(f"Using configuration in '{config}'.", fg="blue") + if ctx.default_map: + for param, value in ctx.default_map.items(): + out(f"{param}: {value}") error_msg = "Oh no! 💥 💔 💥" if ( From 80de2372e4ec385c082c38f365414ac3622a4010 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 10 Dec 2022 15:56:34 -0500 Subject: [PATCH 369/700] Bump mypy[c] from 0.971 to 0.991 (#3380) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 3 +++ pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0be8dc42890..2d5ac26b629 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.991 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index e781cbd523e..86d44f033b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,9 @@ +- Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7 + (#3380) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index 329adaa65d8..aede497e2af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,7 @@ sources = ["src"] enable-by-default = false dependencies = [ "hatch-mypyc>=0.13.0", - "mypy==0.971", + "mypy==0.991", # Required stubs to be removed when the packages support PEP 561 themselves "types-typed-ast>=1.4.2", ] From 9bbe11dd7baf7aea23a5b51d7daf9e12acb3b28c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 06:27:18 -0800 Subject: [PATCH 370/700] Bump pypa/cibuildwheel from 2.11.2 to 2.11.3 (#3434) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.2 to 2.11.3. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.2...v2.11.3) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 16cf90bd206..7fd760ef727 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.11.3 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From abd2b2556a8f9d8efe8914c23520eef7223e2a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 06:28:12 -0800 Subject: [PATCH 371/700] Bump furo from 2022.9.29 to 2022.12.7 in /docs (#3433) Bumps [furo](https://github.com/pradyunsg/furo) from 2022.9.29 to 2022.12.7. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2022.09.29...2022.12.07) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 426a78a7abf..9a269d02a75 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==5.3.0 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.1 -furo==2022.9.29 +furo==2022.12.7 From 7d062ecd5f14124a99daf452c46054ada656ad8b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 12 Dec 2022 20:56:38 -0800 Subject: [PATCH 372/700] Do not put the closing quotes in a docstring on a separate line (#3430) Fixes #3320. Followup from #3044. --- CHANGES.md | 2 ++ src/black/linegen.py | 13 ++++++------- tests/data/preview/docstring_preview.py | 6 ++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 86d44f033b8..f6040359623 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) +- Do not put the closing quotes in a docstring on a separate line, even if the line is + too long (#3430) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 219495e9a5e..644824a3c86 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -389,19 +389,18 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. - # If docstring is one line, then we need to add the length - # of the indent, prefix, and starting quotes. Ending quotes are - # handled later. + # If docstring is one line, we don't put the closing quotes on a + # separate line because it looks ugly (#3320). lines = docstring.splitlines() last_line_length = len(lines[-1]) if docstring else 0 - if len(lines) == 1: - last_line_length += len(indent) + len(prefix) + quote_len - # If adding closing quotes would cause the last line to exceed # the maximum line length then put a line break before the # closing quotes - if last_line_length + quote_len > self.mode.line_length: + if ( + len(lines) > 1 + and last_line_length + quote_len > self.mode.line_length + ): leaf.value = prefix + quote + docstring + "\n" + indent + quote else: leaf.value = prefix + quote + docstring + quote diff --git a/tests/data/preview/docstring_preview.py b/tests/data/preview/docstring_preview.py index 292352c82f3..ff4819acb67 100644 --- a/tests/data/preview/docstring_preview.py +++ b/tests/data/preview/docstring_preview.py @@ -54,13 +54,11 @@ def single_quote_docstring_over_line_limit2(): def docstring_almost_at_line_limit(): - """long docstring................................................................. - """ + """long docstring.................................................................""" def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................ - """ + f"""long docstring................................................................""" def mulitline_docstring_almost_at_line_limit(): From a2821815af5f5a706c673279d6405e286d6e95b8 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 14 Dec 2022 17:56:14 -0800 Subject: [PATCH 373/700] Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439) --- CHANGES.md | 2 ++ src/black/comments.py | 12 +++++++++++- tests/data/simple_cases/fmtonoff5.py | 22 ++++++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f6040359623..1a7c320baf8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ +- Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439) + ### Preview style diff --git a/src/black/comments.py b/src/black/comments.py index dce83abf1bb..e733dccd844 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -232,7 +232,7 @@ def generate_ignored_nodes( # fix for fmt: on in children if children_contains_fmt_on(container, preview=preview): - for child in container.children: + for index, child in enumerate(container.children): if isinstance(child, Leaf) and is_fmt_on(child, preview=preview): if child.type in CLOSING_BRACKETS: # This means `# fmt: on` is placed at a different bracket level @@ -241,6 +241,16 @@ def generate_ignored_nodes( # The alternative is to fail the formatting. yield child return + if ( + child.type == token.INDENT + and index < len(container.children) - 1 + and children_contains_fmt_on( + container.children[index + 1], preview=preview + ) + ): + # This means `# fmt: on` is placed right after an indentation + # level, and we shouldn't swallow the previous INDENT token. + return if children_contains_fmt_on(child, preview=preview): return yield child diff --git a/tests/data/simple_cases/fmtonoff5.py b/tests/data/simple_cases/fmtonoff5.py index 71b1381ed0d..181151b6bd6 100644 --- a/tests/data/simple_cases/fmtonoff5.py +++ b/tests/data/simple_cases/fmtonoff5.py @@ -64,7 +64,7 @@ async def call(param): print ( "This will be formatted" ) -# Regression test for https://github.com/psf/black/issues/2985 +# Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property @@ -75,6 +75,15 @@ def this_will_be_formatted ( self, **kwargs ) -> Named: ... # fmt: on +# Regression test for https://github.com/psf/black/issues/3436. +if x: + return x +# fmt: off +elif unformatted: +# fmt: on + will_be_formatted () + + # output @@ -144,7 +153,7 @@ async def call(param): print("This will be formatted") -# Regression test for https://github.com/psf/black/issues/2985 +# Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property @@ -156,3 +165,12 @@ def this_will_be_formatted(self, **kwargs) -> Named: ... # fmt: on + + +# Regression test for https://github.com/psf/black/issues/3436. +if x: + return x +# fmt: off +elif unformatted: + # fmt: on + will_be_formatted() From 658c8d8d96047c5ba77be4aecc2545a22d5e35b9 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 15 Dec 2022 08:25:28 -0800 Subject: [PATCH 374/700] Improve long values in dict literals (#3440) --- CHANGES.md | 3 ++ src/black/linegen.py | 17 ++++++ src/black/mode.py | 3 ++ src/black/trans.py | 36 ++++++++++--- tests/data/preview/long_dict_values.py | 53 +++++++++++++++++++ tests/data/preview/long_strings.py | 28 ++++++++-- .../data/preview/long_strings__regression.py | 15 ++++++ 7 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 tests/data/preview/long_dict_values.py diff --git a/CHANGES.md b/CHANGES.md index 1a7c320baf8..03c7a286771 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,9 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) +- Long values in dict literals are now wrapped in parentheses; correspondingly + unnecessary parentheses around short values in dict literals are now removed; long + string lambda values are now wrapped in parentheses (#3440) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 644824a3c86..244dbe77eb5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -179,6 +179,23 @@ def visit_stmt( yield from self.visit(child) + def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: + if Preview.wrap_long_dict_values_in_parens in self.mode: + for i, child in enumerate(node.children): + if i == 0: + continue + if node.children[i - 1].type == token.COLON: + if child.type == syms.atom and child.children[0].type == token.LPAR: + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_brackets_around_comma=False, + ): + wrap_in_parentheses(node, child, visible=False) + else: + wrap_in_parentheses(node, child, visible=False) + yield from self.visit_default(node) + def visit_funcdef(self, node: Node) -> Iterator[Line]: """Visit function definition.""" if Preview.annotation_parens not in self.mode: diff --git a/src/black/mode.py b/src/black/mode.py index a3ce20b8619..bcd35b4d4be 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -157,8 +157,11 @@ class Preview(Enum): one_element_subscript = auto() remove_block_trailing_newline = auto() remove_redundant_parens = auto() + # NOTE: string_processing requires wrap_long_dict_values_in_parens + # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() skip_magic_trailing_comma_in_subscript = auto() + wrap_long_dict_values_in_parens = auto() class Deprecated(UserWarning): diff --git a/src/black/trans.py b/src/black/trans.py index 8893ab02aab..b08a6d243d8 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1638,6 +1638,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): * The line is a dictionary key assignment where some valid key is being assigned the value of some string. OR + * The line is an lambda expression and the value is a string. + OR * The line starts with an "atom" string that prefers to be wrapped in parens. It's preferred to be wrapped when the string is surrounded by commas (or is the first/last child). @@ -1683,7 +1685,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult: or self._else_match(LL) or self._assert_match(LL) or self._assign_match(LL) - or self._dict_match(LL) + or self._dict_or_lambda_match(LL) or self._prefer_paren_wrap_match(LL) ) @@ -1841,22 +1843,23 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: return None @staticmethod - def _dict_match(LL: List[Leaf]) -> Optional[int]: + def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: """ Returns: string_idx such that @LL[string_idx] is equal to our target (i.e. matched) string, if this line matches the dictionary key assignment - statement requirements listed in the 'Requirements' section of this - classes' docstring. + statement or lambda expression requirements listed in the + 'Requirements' section of this classes' docstring. OR None, otherwise. """ - # If this line is apart of a dictionary key assignment... - if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]: + # If this line is a part of a dictionary key assignment or lambda expression... + parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)] + if syms.dictsetmaker in parent_types or syms.lambdef in parent_types: is_valid_index = is_valid_index_factory(LL) for i, leaf in enumerate(LL): - # We MUST find a colon... + # We MUST find a colon, it can either be dict's or lambda's colon... if leaf.type == token.COLON: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 @@ -1951,6 +1954,25 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: f" (left_leaves={left_leaves}, right_leaves={right_leaves})" ) old_rpar_leaf = right_leaves.pop() + elif right_leaves and right_leaves[-1].type == token.RPAR: + # Special case for lambda expressions as dict's value, e.g.: + # my_dict = { + # "key": lambda x: f"formatted: {x}, + # } + # After wrapping the dict's value with parentheses, the string is + # followed by a RPAR but its opening bracket is lambda's, not + # the string's: + # "key": (lambda x: f"formatted: {x}), + opening_bracket = right_leaves[-1].opening_bracket + if opening_bracket is not None and opening_bracket in left_leaves: + index = left_leaves.index(opening_bracket) + if ( + index > 0 + and index < len(left_leaves) - 1 + and left_leaves[index - 1].type == token.COLON + and left_leaves[index + 1].value == "lambda" + ): + right_leaves.pop() append_leaves(string_line, line, right_leaves) diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py new file mode 100644 index 00000000000..f23c5d3dad1 --- /dev/null +++ b/tests/data/preview/long_dict_values.py @@ -0,0 +1,53 @@ +my_dict = { + "something_something": + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", +} + +my_dict = { + "a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0 +} + +my_dict = { + "a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0 +} + +my_dict = { + "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") +} + + +# output + + +my_dict = { + "something_something": ( + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + ), +} + +my_dict = { + "a key in my dict": ( + a_very_long_variable * and_a_very_long_function_call() / 100000.0 + ) +} + +my_dict = { + "a key in my dict": ( + a_very_long_variable + * and_a_very_long_function_call() + * and_another_long_func() + / 100000.0 + ) +} + +my_dict = { + "a key in my dict": ( + MyClass.some_attribute.first_call() + .second_call() + .third_call(some_args="some value") + ) +} diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 9288b253b60..9c78f675b8f 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -278,6 +278,15 @@ def foo(): "........................................................................... \\N{LAO KO LA}" ) +msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line" + +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} + # output @@ -362,9 +371,8 @@ def foo(): "A %s %s" % ("formatted", "string"): ( "This is a really really really long string that has to go inside of a" - " dictionary. It is %s bad (#%d)." - ) - % ("soooo", 2), + " dictionary. It is %s bad (#%d)." % ("soooo", 2) + ), } D5 = { # Test for https://github.com/psf/black/issues/3261 @@ -806,3 +814,17 @@ def foo(): "..........................................................................." " \\N{LAO KO LA}" ) + +msg = ( + lambda x: ( + f"this is a very very very long lambda value {x} that doesn't fit on a single" + " line" + ) +) + +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 8b00e76f40e..6d56dcc635d 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -524,6 +524,13 @@ async def foo(self): }, ) +# Regression test for https://github.com/psf/black/issues/3117. +some_dict = { + "something_something": + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", +} + # output @@ -1178,3 +1185,11 @@ async def foo(self): ), }, ) + +# Regression test for https://github.com/psf/black/issues/3117. +some_dict = { + "something_something": ( + r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" + r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" + ), +} From aafc21aa77f5c4d2ebcb833aa60faba6c2138b94 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 15 Dec 2022 15:58:51 -0800 Subject: [PATCH 375/700] Prefer splitting right hand side of assignment statements. (#3368) --- CHANGES.md | 2 + src/black/linegen.py | 122 +++++++++++++++--- src/black/mode.py | 1 + .../data/preview/long_strings__regression.py | 6 +- tests/data/preview/prefer_rhs_split.py | 85 ++++++++++++ .../preview/prefer_rhs_split_reformatted.py | 38 ++++++ 6 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 tests/data/preview/prefer_rhs_split.py create mode 100644 tests/data/preview/prefer_rhs_split_reformatted.py diff --git a/CHANGES.md b/CHANGES.md index 03c7a286771..e1ad5e1f1cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,8 @@ present) or as a single newline character (if a newline is present) (#3348) - Implicitly concatenated strings used as function args are now wrapped inside parentheses (#3307) +- For assignment statements, prefer splitting the right hand side if the left hand side + fits on a single line (#3368) - Correctly handle trailing commas that are inside a line's leading non-nested parens (#3370) diff --git a/src/black/linegen.py b/src/black/linegen.py index 244dbe77eb5..91223747618 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,6 +2,7 @@ Generating lines of code. """ import sys +from dataclasses import dataclass from enum import Enum, auto from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast @@ -24,6 +25,7 @@ from black.mode import Feature, Mode, Preview from black.nodes import ( ASSIGNMENTS, + BRACKETS, CLOSING_BRACKETS, OPENING_BRACKETS, RARROW, @@ -634,6 +636,17 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator yield result +@dataclass +class _RHSResult: + """Intermediate split result from a right hand split.""" + + head: Line + body: Line + tail: Line + opening_bracket: Leaf + closing_bracket: Leaf + + def right_hand_split( line: Line, line_length: int, @@ -648,6 +661,22 @@ def right_hand_split( Note: running this function modifies `bracket_depth` on the leaves of `line`. """ + rhs_result = _first_right_hand_split(line, omit=omit) + yield from _maybe_split_omitting_optional_parens( + rhs_result, line, line_length, features=features, omit=omit + ) + + +def _first_right_hand_split( + line: Line, + omit: Collection[LeafID] = (), +) -> _RHSResult: + """Split the line into head, body, tail starting with the last bracket pair. + + Note: this function should not have side effects. It's relied upon by + _maybe_split_omitting_optional_parens to get an opinion whether to prefer + splitting on the right side of an assignment statement. + """ tail_leaves: List[Leaf] = [] body_leaves: List[Leaf] = [] head_leaves: List[Leaf] = [] @@ -683,37 +712,71 @@ def right_hand_split( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) bracket_split_succeeded_or_raise(head, body, tail) + return _RHSResult(head, body, tail, opening_bracket, closing_bracket) + + +def _maybe_split_omitting_optional_parens( + rhs: _RHSResult, + line: Line, + line_length: int, + features: Collection[Feature] = (), + omit: Collection[LeafID] = (), +) -> Iterator[Line]: if ( Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren - and opening_bracket.type == token.LPAR - and not opening_bracket.value + and rhs.opening_bracket.type == token.LPAR + and not rhs.opening_bracket.value # the closing bracket is an optional paren - and closing_bracket.type == token.RPAR - and not closing_bracket.value + and rhs.closing_bracket.type == token.RPAR + and not rhs.closing_bracket.value # it's not an import (optional parens are the only thing we can split on # in this case; attempting a split without them is a waste of time) and not line.is_import # there are no standalone comments in the body - and not body.contains_standalone_comments(0) + and not rhs.body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(body, line_length) + and can_omit_invisible_parens(rhs.body, line_length) ): - omit = {id(closing_bracket), *omit} + omit = {id(rhs.closing_bracket), *omit} try: - yield from right_hand_split(line, line_length, features=features, omit=omit) - return + # The _RHSResult Omitting Optional Parens. + rhs_oop = _first_right_hand_split(line, omit=omit) + if not ( + Preview.prefer_splitting_right_hand_side_of_assignments in line.mode + # the split is right after `=` + and len(rhs.head.leaves) >= 2 + and rhs.head.leaves[-2].type == token.EQUAL + # the left side of assignement contains brackets + and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) + # the left side of assignment is short enough (the -1 is for the ending + # optional paren) + and is_line_short_enough(rhs.head, line_length=line_length - 1) + # the left side of assignment won't explode further because of magic + # trailing comma + and rhs.head.magic_trailing_comma is None + # the split by omitting optional parens isn't preferred by some other + # reason + and not _prefer_split_rhs_oop(rhs_oop, line_length=line_length) + ): + yield from _maybe_split_omitting_optional_parens( + rhs_oop, line, line_length, features=features, omit=omit + ) + return except CannotSplit as e: if not ( - can_be_split(body) - or is_line_short_enough(body, line_length=line_length) + can_be_split(rhs.body) + or is_line_short_enough(rhs.body, line_length=line_length) ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." ) from e - elif head.contains_multiline_strings() or tail.contains_multiline_strings(): + elif ( + rhs.head.contains_multiline_strings() + or rhs.tail.contains_multiline_strings() + ): raise CannotSplit( "The current optional pair of parentheses is bound to fail to" " satisfy the splitting algorithm because the head or the tail" @@ -721,13 +784,42 @@ def right_hand_split( " line." ) from e - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - for result in (head, body, tail): + ensure_visible(rhs.opening_bracket) + ensure_visible(rhs.closing_bracket) + for result in (rhs.head, rhs.body, rhs.tail): if result: yield result +def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: + """ + Returns whether we should prefer the result from a split omitting optional parens. + """ + has_closing_bracket_after_assign = False + for leaf in reversed(rhs_oop.head.leaves): + if leaf.type == token.EQUAL: + break + if leaf.type in CLOSING_BRACKETS: + has_closing_bracket_after_assign = True + break + return ( + # contains matching brackets after the `=` (done by checking there is a + # closing bracket) + has_closing_bracket_after_assign + or ( + # the split is actually from inside the optional parens (done by checking + # the first line still contains the `=`) + any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) + # the first line is short enough + and is_line_short_enough(rhs_oop.head, line_length=line_length) + ) + # contains unsplittable type ignore + or rhs_oop.head.contains_unsplittable_type_ignore() + or rhs_oop.body.contains_unsplittable_type_ignore() + or rhs_oop.tail.contains_unsplittable_type_ignore() + ) + + def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None: """Raise :exc:`CannotSplit` if the last left- or right-hand split failed. diff --git a/src/black/mode.py b/src/black/mode.py index bcd35b4d4be..a104d1b9862 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -155,6 +155,7 @@ class Preview(Enum): long_docstring_quotes_on_newline = auto() normalize_docstring_quotes_and_prefixes_properly = auto() one_element_subscript = auto() + prefer_splitting_right_hand_side_of_assignments = auto() remove_block_trailing_newline = auto() remove_redundant_parens = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 6d56dcc635d..8b8fc179147 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -983,9 +983,9 @@ def xxxxxxx_xxxxxx(xxxx): ) -value.__dict__[ - key -] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) RE_ONE_BACKSLASH = { "asdf_hjkl_jkl": re.compile( diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py new file mode 100644 index 00000000000..5b89113e618 --- /dev/null +++ b/tests/data/preview/prefer_rhs_split.py @@ -0,0 +1,85 @@ +first_item, second_item = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +some_dict["with_a_long_key"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +# Make sure it works when the RHS only has one pair of (optional) parens. +first_item, second_item = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) + +some_dict["with_a_long_key"] = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) + +# Make sure chaining assignments work. +first_item, second_item, third_item, forth_item = m["everything"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) + +# Make sure when the RHS's first split at the non-optional paren fits, +# we split there instead of the outer RHS optional paren. +first_item, second_item = some_looooooooong_module.some_loooooog_function_name( + first_argument, second_argument, third_argument +) + +( + first_item, + second_item, + third_item, + forth_item, + fifth_item, + last_item_very_loooooong, +) = some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument +) + +( + first_item, + second_item, + third_item, + forth_item, + fifth_item, + last_item_very_loooooong, +) = everyting = some_loooooog_function_name( + first_argument, second_argument, third_argument +) + + +# Make sure unsplittable type ignore won't be moved. +some_kind_of_table[some_key] = util.some_function( # type: ignore # noqa: E501 + some_arg +).intersection(pk_cols) + +some_kind_of_table[ + some_key +] = lambda obj: obj.some_long_named_method() # type: ignore # noqa: E501 + +some_kind_of_table[ + some_key # type: ignore # noqa: E501 +] = lambda obj: obj.some_long_named_method() + + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[ + xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) +] = 1 + + +# Right side of assignment contains un-nested pairs of inner parens. +some_kind_of_instance.some_kind_of_map[a_key] = ( + isinstance(some_var, SomeClass) + and table.something_and_something != table.something_else +) or ( + isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing +) diff --git a/tests/data/preview/prefer_rhs_split_reformatted.py b/tests/data/preview/prefer_rhs_split_reformatted.py new file mode 100644 index 00000000000..781e75be0aa --- /dev/null +++ b/tests/data/preview/prefer_rhs_split_reformatted.py @@ -0,0 +1,38 @@ +# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. + +# Left hand side fits in a single line but will still be exploded by the +# magic trailing comma. +first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( + arg1, + arg2, +) + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 + + +# output + + +# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. + +# Left hand side fits in a single line but will still be exploded by the +# magic trailing comma. +( + first_value, + ( + m1, + m2, + ), + third_value, +) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( + arg1, + arg2, +) + +# Make when when the left side of assignement plus the opening paren "... = (" is +# exactly line length limit + 1, it won't be split like that. +xxxxxxxxx_yyy_zzzzzzzz[ + xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) +] = 1 From 78163939f157d9e18a8c0528fc5e1c58b1c1e69c Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 16 Dec 2022 05:02:41 -0800 Subject: [PATCH 376/700] Fix an infinite recursion error exposed by #3440 (#3444) --- src/black/linegen.py | 6 +++-- tests/data/preview/long_dict_values.py | 37 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 91223747618..fe6ea11c501 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1417,8 +1417,10 @@ def run_transformer( result.extend(transform_line(transformed_line, mode=mode, features=features)) + features_set = set(features) if ( - transform.__class__.__name__ != "rhs" + Feature.FORCE_OPTIONAL_PARENTHESES in features_set + or transform.__class__.__name__ != "rhs" or not line.bracket_tracker.invisible or any(bracket.value for bracket in line.bracket_tracker.invisible) or line.contains_multiline_strings() @@ -1435,7 +1437,7 @@ def run_transformer( line_copy = line.clone() append_leaves(line_copy, line, line.leaves) - features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} + features_fop = features_set | {Feature.FORCE_OPTIONAL_PARENTHESES} second_opinion = run_transformer( line_copy, transform, mode, features_fop, line_str=line_str ) diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py index f23c5d3dad1..4c515180028 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/preview/long_dict_values.py @@ -17,6 +17,24 @@ "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") } +{ + 'xxxxxx': + xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx( + xxxxxxxxxxxxxx={ + 'x': + xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={ + 'x': x.xx, + 'x': x.x, + })))) + }), +} + # output @@ -51,3 +69,22 @@ .third_call(some_args="some value") ) } + +{ + "xxxxxx": xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx( + xxxxxxxxxxxxxx={ + "x": xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={ + "x": x.xx, + "x": x.x, + } + ) + ) + ) + ) + } + ), +} From c0089ef19dd12f872c581f106b1236c46d609955 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 17 Dec 2022 09:19:45 -0800 Subject: [PATCH 377/700] Remove separate 3.11 CI now deps support 3.11 (#3446) * Remove separate 3.11 CI now deps support 3.11 - We can run everything now like all other stable versions of Python - test in a 3.11 vent: `/tmp/tb/bin/tox -e py311,ci-py311` ``` py311: OK (28.99=setup[7.90]+cmd[5.29,0.66,6.94,6.08,1.89,0.24] seconds) ci-py311: OK (30.33=setup[3.20]+cmd[3.66,0.31,17.43,4.60,0.90,0.23] seconds) congratulations :) (59.35 seconds) ``` * Add to CHANGES.md * Add fuzz run in 3.11 --- .github/workflows/fuzz.yml | 2 +- .github/workflows/test-311.yml | 57 ---------------------------------- .github/workflows/test.yml | 2 +- CHANGES.md | 2 ++ 4 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/test-311.yml diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ebb8a9fda9e..373e1500ee9 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml deleted file mode 100644 index c2da2465ad5..00000000000 --- a/.github/workflows/test-311.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Test 3.11 without aiohttp extensions - -on: - push: - paths-ignore: - - "docs/**" - - "*.md" - - pull_request: - paths-ignore: - - "docs/**" - - "*.md" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - main: - # 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.11.0-rc - 3.11"] - os: [ubuntu-latest, macOS-latest, windows-latest] - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install tox - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade tox - - - name: Run tests via tox - run: | - python -m tox -e 311 - - - name: Format ourselves - run: | - python -m pip install . - python -m black --check src/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 372d1fd5d38..3ca2a469147 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.7", "pypy-3.8"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index e1ad5e1f1cc..ba71ee6c99d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -57,6 +57,8 @@ +- Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446) + ### Documentation - Fix a crash in preview style with assert + parenthesized string (#3415) +- Fix crashes in preview style with walrus operators used in function return annotations + and except clauses (#3423) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/linegen.py b/src/black/linegen.py index fe6ea11c501..2e75bc94506 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1268,6 +1268,8 @@ def maybe_make_parens_invisible_in_atom( syms.expr_stmt, syms.assert_stmt, syms.return_stmt, + syms.except_clause, + syms.funcdef, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, diff --git a/tests/data/py_310/pattern_matching_extras.py b/tests/data/py_310/pattern_matching_extras.py index 9f6907f7575..0242d264e5b 100644 --- a/tests/data/py_310/pattern_matching_extras.py +++ b/tests/data/py_310/pattern_matching_extras.py @@ -114,6 +114,6 @@ def func(match: case, case: match) -> case: match bar1: case Foo( - normal=x, perhaps=[list, {an: d, dict: 1.0}] as y, otherwise=something, q=t as u + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u ): pass diff --git a/tests/util.py b/tests/util.py index d65c2e651ae..967d576fafe 100644 --- a/tests/util.py +++ b/tests/util.py @@ -2,6 +2,7 @@ import sys import unittest from contextlib import contextmanager +from dataclasses import replace from functools import partial from pathlib import Path from typing import Any, Iterator, List, Optional, Tuple @@ -56,6 +57,10 @@ def _assert_format_equal(expected: str, actual: str) -> None: assert actual == expected +class FormatFailure(Exception): + """Used to wrap failures when assert_format() runs in an extra mode.""" + + def assert_format( source: str, expected: str, @@ -70,12 +75,57 @@ def assert_format( safety guards so they don't just crash with a SyntaxError. Please note this is separate from TargetVerson Mode configuration. """ + _assert_format_inner( + source, expected, mode, fast=fast, minimum_version=minimum_version + ) + + # For both preview and non-preview tests, ensure that Black doesn't crash on + # this code, but don't pass "expected" because the precise output may differ. + try: + _assert_format_inner( + source, + None, + replace(mode, preview=not mode.preview), + fast=fast, + minimum_version=minimum_version, + ) + except Exception as e: + text = "non-preview" if mode.preview else "preview" + raise FormatFailure( + f"Black crashed formatting this case in {text} mode." + ) from e + # Similarly, setting line length to 1 is a good way to catch + # stability bugs. But only in non-preview mode because preview mode + # currently has a lot of line length 1 bugs. + try: + _assert_format_inner( + source, + None, + replace(mode, preview=False, line_length=1), + fast=fast, + minimum_version=minimum_version, + ) + except Exception as e: + raise FormatFailure( + "Black crashed formatting this case with line-length set to 1." + ) from e + + +def _assert_format_inner( + source: str, + expected: Optional[str] = None, + mode: black.Mode = DEFAULT_MODE, + *, + fast: bool = False, + minimum_version: Optional[Tuple[int, int]] = None, +) -> None: actual = black.format_str(source, mode=mode) - _assert_format_equal(expected, actual) + if expected is not None: + _assert_format_equal(expected, actual) # It's not useful to run safety checks if we're expecting no changes anyway. The # assertion right above will raise if reality does actually make changes. This just # avoids wasted CPU cycles. - if not fast and source != expected: + if not fast and source != actual: # Unfortunately the AST equivalence check relies on the built-in ast module # being able to parse the code being formatted. This doesn't always work out # when checking modern code on older versions. From 23b92b48a5e6b39d507c84839ecc0decb7f01a34 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 17 Dec 2022 14:51:10 -0800 Subject: [PATCH 379/700] Fix syntax error in match test (#3426) From cd9fef8bab3a39dfba494f1917e4158faba439f9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 17 Dec 2022 19:02:01 -0800 Subject: [PATCH 380/700] tomli: Don't worry about specific alpha releases (#3448) This prevents bugs due to pypa/packaging#522. Fixes #3447. --- CHANGES.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 50c0f5df050..f786f1a1fed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,9 @@ - Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7 (#3380) +- Drop specific support for the `tomli` requirement on 3.11 alpha releases, working + around a bug that would cause the requirement not to be installed on any non-final + Python releases (#3448) ### Parser diff --git a/pyproject.toml b/pyproject.toml index aede497e2af..ab38908ba15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ "mypy_extensions>=0.4.3", "pathspec>=0.9.0", "platformdirs>=2", - "tomli>=1.1.0; python_full_version < '3.11.0a7'", + "tomli>=1.1.0; python_version < '3.11'", "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "typing_extensions>=3.10.0.0; python_version < '3.10'", ] From 9ce75726fcf1f3a2617e873d925384fd6b876dcb Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 19 Dec 2022 18:08:13 -0800 Subject: [PATCH 381/700] Do not move docker `latest_release` tag for Pre-Releases (#3461) * Do not move docker `latest_release` tag for Pre-Releases - When we do a pre-release lets not move the latest_release tag - This tag should only move on official real releases Fixes #3453 * Make it prettier - TIL we format our yaml --- .github/workflows/docker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a3106d04aae..855186f9bf1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -44,7 +44,9 @@ jobs: tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - name: Build and push latest_release tag - if: ${{ github.event_name == 'release' && github.event.action == 'published' }} + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + !github.event.release.prerelease }} uses: docker/build-push-action@v3 with: context: . From 1e8217fd6284bdb020e7ca70964d677a3016f914 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 06:36:42 -0800 Subject: [PATCH 382/700] Fix an f-string crash in ESP. (#3463) --- CHANGES.md | 2 ++ src/black/trans.py | 9 ++++-- .../data/preview/long_strings__regression.py | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f786f1a1fed..61608c361cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations and except clauses (#3423) +- Fix a crash in preview advanced string processing where mixed implicitly concatenated + regular and f-strings start with an empty span (#3463) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/trans.py b/src/black/trans.py index b08a6d243d8..25d35afe74d 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1359,9 +1359,14 @@ def more_splits_should_be_made() -> bool: # prefix, and the current custom split did NOT originally use a # prefix... if ( - next_value != self._normalize_f_string(next_value, prefix) - and use_custom_breakpoints + use_custom_breakpoints and not csplit.has_prefix + and ( + # `next_value == prefix + QUOTE` happens when the custom + # split is an empty string. + next_value == prefix + QUOTE + or next_value != self._normalize_f_string(next_value, prefix) + ) ): # Then `csplit.break_idx` will be off by one after removing # the 'f' prefix. diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 8b8fc179147..5e8f012bc3e 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -531,6 +531,18 @@ async def foo(self): r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t", } +# Regression test for https://github.com/psf/black/issues/3459. +xxxx( + empty_str_as_first_split='' + f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx ' + 'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. ' + f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', + empty_u_str_as_first_split=u'' + f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx ' + 'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. ' + f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', +) + # output @@ -1193,3 +1205,19 @@ async def foo(self): r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" ), } + +# Regression test for https://github.com/psf/black/issues/3459. +xxxx( + empty_str_as_first_split=( + "" + f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx " + "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. " + f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" + ), + empty_u_str_as_first_split=( + "" + f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx " + "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. " + f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" + ), +) From a44dc3d59eb46901f9fe893727280903df41fc20 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 13:38:35 -0800 Subject: [PATCH 383/700] Exclude string type annotations from ESP (#3462) --- CHANGES.md | 2 + src/black/brackets.py | 16 ++++- src/black/nodes.py | 12 ++++ src/black/trans.py | 23 ++++---- .../preview/long_strings__type_annotations.py | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 tests/data/preview/long_strings__type_annotations.py diff --git a/CHANGES.md b/CHANGES.md index 61608c361cb..c29933fe5d9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - Long values in dict literals are now wrapped in parentheses; correspondingly unnecessary parentheses around short values in dict literals are now removed; long string lambda values are now wrapped in parentheses (#3440) +- Exclude string type annotations from improved string processing; fix crash when the + return type annotation is stringified and spans across multiple lines (#3462) ### Configuration diff --git a/src/black/brackets.py b/src/black/brackets.py index ec9708cb08a..343f0608d50 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -80,9 +80,12 @@ def mark(self, leaf: Leaf) -> None: within brackets a given leaf is. 0 means there are no enclosing brackets that started on this line. - If a leaf is itself a closing bracket, it receives an `opening_bracket` - field that it forms a pair with. This is a one-directional link to - avoid reference cycles. + If a leaf is itself a closing bracket and there is a matching opening + bracket earlier, it receives an `opening_bracket` field with which it forms a + pair. This is a one-directional link to avoid reference cycles. Closing + bracket without opening happens on lines continued from previous + breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place + the return type annotation on its own line of the previous closing RPAR. If a leaf is a delimiter (a token on which Black can split the line if needed) and it's on depth 0, its `id()` is stored in the tracker's @@ -91,6 +94,13 @@ def mark(self, leaf: Leaf) -> None: if leaf.type == token.COMMENT: return + if ( + self.depth == 0 + and leaf.type in CLOSING_BRACKETS + and (self.depth, leaf.type) not in self.bracket_match + ): + return + self.maybe_decrement_after_for_loop_variable(leaf) self.maybe_decrement_after_lambda_arguments(leaf) if leaf.type in CLOSING_BRACKETS: diff --git a/src/black/nodes.py b/src/black/nodes.py index aeb2be389c8..a11fb7cc071 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -848,3 +848,15 @@ def is_string_token(nl: NL) -> TypeGuard[Leaf]: def is_number_token(nl: NL) -> TypeGuard[Leaf]: return nl.type == token.NUMBER + + +def is_part_of_annotation(leaf: Leaf) -> bool: + """Returns whether this leaf is part of type annotations.""" + ancestor = leaf.parent + while ancestor is not None: + if ancestor.prev_sibling and ancestor.prev_sibling.type == token.RARROW: + return True + if ancestor.parent and ancestor.parent.type == syms.tname: + return True + ancestor = ancestor.parent + return False diff --git a/src/black/trans.py b/src/black/trans.py index 25d35afe74d..a5cf4955f13 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -30,7 +30,6 @@ from mypy_extensions import trait -from black.brackets import BracketMatchError from black.comments import contains_pragma_comment from black.lines import Line, append_leaves from black.mode import Feature @@ -41,6 +40,7 @@ is_empty_lpar, is_empty_par, is_empty_rpar, + is_part_of_annotation, parent_type, replace_child, syms, @@ -351,7 +351,7 @@ class StringMerger(StringTransformer, CustomSplitMapMixin): Requirements: (A) The line contains adjacent strings such that ALL of the validation checks - listed in StringMerger.__validate_msg(...)'s docstring pass. + listed in StringMerger._validate_msg(...)'s docstring pass. OR (B) The line contains a string which uses line continuation backslashes. @@ -377,6 +377,8 @@ def do_match(self, line: Line) -> TMatchResult: and is_valid_index(i + 1) and LL[i + 1].type == token.STRING ): + if is_part_of_annotation(leaf): + return TErr("String is part of type annotation.") return Ok(i) if leaf.type == token.STRING and "\\\n" in leaf.value: @@ -454,7 +456,7 @@ def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: Returns: Ok(new_line), if ALL of the validation checks found in - __validate_msg(...) pass. + _validate_msg(...) pass. OR Err(CannotTransform), otherwise. """ @@ -608,7 +610,7 @@ def make_naked(string: str, string_prefix: str) -> str: def _validate_msg(line: Line, string_idx: int) -> TResult[None]: """Validate (M)erge (S)tring (G)roup - Transform-time string validation logic for __merge_string_group(...). + Transform-time string validation logic for _merge_string_group(...). Returns: * Ok(None), if ALL validation checks (listed below) pass. @@ -622,6 +624,11 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: - The set of all string prefixes in the string group is of length greater than one and is not equal to {"", "f"}. - The string group consists of raw strings. + - The string group is stringified type annotations. We don't want to + process stringified type annotations since pyright doesn't support + them spanning multiple string values. (NOTE: mypy, pytype, pyre do + support them, so we can change if pyright also gains support in the + future. See https://github.com/microsoft/pyright/issues/4359.) """ # We first check for "inner" stand-alone comments (i.e. stand-alone # comments that have a string leaf before them AND after them). @@ -812,13 +819,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: new_line = line.clone() new_line.comments = line.comments.copy() - try: - append_leaves(new_line, line, LL[: string_idx - 1]) - except BracketMatchError: - # HACK: I believe there is currently a bug somewhere in - # right_hand_split() that is causing brackets to not be tracked - # properly by a shared BracketTracker. - append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True) + append_leaves(new_line, line, LL[: string_idx - 1]) string_leaf = Leaf(token.STRING, LL[string_idx].value) LL[string_idx - 1].remove() diff --git a/tests/data/preview/long_strings__type_annotations.py b/tests/data/preview/long_strings__type_annotations.py new file mode 100644 index 00000000000..41d7ee2b67b --- /dev/null +++ b/tests/data/preview/long_strings__type_annotations.py @@ -0,0 +1,59 @@ +def func( + arg1, + arg2, +) -> Set["this_is_a_very_long_module_name.AndAVeryLongClasName" + ".WithAVeryVeryVeryVeryVeryLongSubClassName"]: + pass + + +def func( + argument: ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" + ), +) -> ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" +): + pass + + +def func( + argument: ( + "int |" + "str" + ), +) -> Set["int |" + " str"]: + pass + + +# output + + +def func( + arg1, + arg2, +) -> Set[ + "this_is_a_very_long_module_name.AndAVeryLongClasName" + ".WithAVeryVeryVeryVeryVeryLongSubClassName" +]: + pass + + +def func( + argument: ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" + ), +) -> ( + "VeryLongClassNameWithAwkwardGenericSubtype[int] |" + "VeryLongClassNameWithAwkwardGenericSubtype[str]" +): + pass + + +def func( + argument: ("int |" "str"), +) -> Set["int |" " str"]: + pass From 73c2d5514ce604141abe176b1f3e5cd35ff51d56 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 14:59:38 -0800 Subject: [PATCH 384/700] Fix a crash in ESP where a standalone comment is placed before a dict's value (#3469) --- CHANGES.md | 2 ++ src/black/trans.py | 2 +- tests/data/preview/long_strings__regression.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c29933fe5d9..1e51f3e9108 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ and except clauses (#3423) - Fix a crash in preview advanced string processing where mixed implicitly concatenated regular and f-strings start with an empty span (#3463) +- Fix a crash in preview advanced string processing where a standalone comment is placed + before a dict's value (#3469) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/trans.py b/src/black/trans.py index a5cf4955f13..0eb53e2b098 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1866,7 +1866,7 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: for i, leaf in enumerate(LL): # We MUST find a colon, it can either be dict's or lambda's colon... - if leaf.type == token.COLON: + if leaf.type == token.COLON and i < len(LL) - 1: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 # That colon MUST be followed by a string... diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 5e8f012bc3e..ef9007f4ce1 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -543,6 +543,13 @@ async def foo(self): f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}', ) +# Regression test for https://github.com/psf/black/issues/3455. +a_dict = { + "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces": + # And there is a comment before the value + ("item1", "item2", "item3"), +} + # output @@ -1221,3 +1228,10 @@ async def foo(self): f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}" ), ) + +# Regression test for https://github.com/psf/black/issues/3455. +a_dict = { + "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces": + # And there is a comment before the value + ("item1", "item2", "item3"), +} From 59f03d1b9d3c77f214087948b4d5e3dbb024d0b3 Mon Sep 17 00:00:00 2001 From: Matthew Armand Date: Tue, 20 Dec 2022 18:00:06 -0500 Subject: [PATCH 385/700] Vim plugin docs improvements (#3468) * Organize vim plugin section with headers to separate out Installation, Usage, and Troubleshooting for readability and easy linking * Add missing plugin configuration options, with current defaults * Add installation note for Arch Linux, now that the plugin is shipped with the python-black package (ref: https://bugs.archlinux.org/task/73024) * Fix vim-plug specification to follow stable releases. Moving the same tag is an antipattern that doesn't re-resolve with vim-plug, see this discussion for more detail (https://github.com/junegunn/vim-plug/pull/720\#issuecomment-1105829356). Per vim-plug's maintainer's recommendation, use the 'tag' key instead with a shell wildcard. Wildcard should be '*.*.*' as that follows Black's versioning detailed here (https://black.readthedocs.io/en/latest/contributing/release_process.html\#cutting-a-release) and doesn't include current alpha releases. --- CHANGES.md | 2 ++ docs/integrations/editors.md | 49 +++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e51f3e9108..e2c5adfda35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -75,6 +75,8 @@ +- Expand `vim-plug` installation instructions to offer more explicit options (#3468) + ## 22.12.0 ### Preview style diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 0778c6a72f1..a8b7978c4d7 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -111,16 +111,51 @@ Configuration: - `g:black_fast` (defaults to `0`) - `g:black_linelength` (defaults to `88`) - `g:black_skip_string_normalization` (defaults to `0`) +- `g:black_skip_magic_trailing_comma` (defaults to `0`) - `g:black_virtualenv` (defaults to `~/.vim/black` or `~/.local/share/nvim/black`) +- `g:black_use_virtualenv` (defaults to `1`) +- `g:black_target_version` (defaults to `""`) - `g:black_quiet` (defaults to `0`) - `g:black_preview` (defaults to `0`) +#### Installation + +This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to +be able to run _Black_ inside the Vim process which is much faster than calling an +external command. + +##### `vim-plug` + To install with [vim-plug](https://github.com/junegunn/vim-plug): +_Black_'s `stable` branch tracks official version updates, and can be used to simply +follow the most recent stable version. + ``` Plug 'psf/black', { 'branch': 'stable' } ``` +Another option which is a bit more explicit and offers more control is to use +`vim-plug`'s `tag` option with a shell wildcard. This will resolve to the latest tag +which matches the given pattern. + +The following matches all stable versions (see the +[Release Process](../contributing/release_process.md) section for documentation of +version scheme used by Black): + +``` +Plug 'psf/black', { 'tag': '*.*.*' } +``` + +and the following demonstrates pinning to a specific year's stable style (2022 in this +case): + +``` +Plug 'psf/black', { 'tag': '22.*.*' } +``` + +##### Vundle + or with [Vundle](https://github.com/VundleVim/Vundle.vim): ``` @@ -134,6 +169,14 @@ $ cd ~/.vim/bundle/black $ git checkout origin/stable -b stable ``` +##### Arch Linux + +On Arch Linux, the plugin is shipped with the +[`python-black`](https://archlinux.org/packages/community/any/python-black/) package, so +you can start using it in Vim after install with no additional setup. + +##### Vim 8 Native Plugin Management + or you can copy the plugin files from [plugin/black.vim](https://github.com/psf/black/blob/stable/plugin/black.vim) and [autoload/black.vim](https://github.com/psf/black/blob/stable/autoload/black.vim). @@ -148,9 +191,7 @@ curl https://raw.githubusercontent.com/psf/black/stable/autoload/black.vim -o ~/ Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or Pathogen, and so on. -This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to -be able to run _Black_ inside the Vim process which is much faster than calling an -external command. +#### Usage On first run, the plugin creates its own virtualenv using the right Python version and automatically installs _Black_. You can upgrade it later by calling `:BlackUpgrade` and @@ -187,6 +228,8 @@ To run _Black_ on a key press (e.g. F9 below), add this: nnoremap :Black ``` +#### Troubleshooting + **How to get Vim with Python 3.6?** On Ubuntu 17.10 Vim comes with Python 3.6 by default. On macOS with Homebrew run: `brew install vim`. When building Vim from source, use: `./configure --enable-python3interp=yes`. There's many guides online how to do From 29dd25725303992d36c3a75c3a071080ac06085f Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 20 Dec 2022 17:58:02 -0800 Subject: [PATCH 386/700] Fix an issue where extra empty lines are added. (#3470) --- CHANGES.md | 2 ++ src/black/lines.py | 3 +- tests/data/preview/comments9.py | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e2c5adfda35..c07d81d1320 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ regular and f-strings start with an empty span (#3463) - Fix a crash in preview advanced string processing where a standalone comment is placed before a dict's value (#3469) +- Fix an issue where extra empty lines are added when a decorator has `# fmt: skip` + applied or there is a standalone comment between decorators (#3470) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/lines.py b/src/black/lines.py index 08281bcf370..2aa675c3b31 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -520,7 +520,8 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and (self.semantic_leading_comment is None or before) ): self.semantic_leading_comment = block - elif not current_line.is_decorator: + # `or before` means this decorator already has an empty line before + elif not current_line.is_decorator or before: self.semantic_leading_comment = None self.previous_line = current_line diff --git a/tests/data/preview/comments9.py b/tests/data/preview/comments9.py index 449612c037a..77b25556e74 100644 --- a/tests/data/preview/comments9.py +++ b/tests/data/preview/comments9.py @@ -114,6 +114,31 @@ def first_method(self): pass +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function + + +@decorator1 +@decorator2 # fmt: skip +def bar(): + pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function. + # NOTE this comment only has one empty line below, and the formatter + # should enforce two blank lines. + +@decorator1 +# A standalone comment +def bar(): + pass + + # output @@ -252,3 +277,29 @@ class MyClass: # More comments. def first_method(self): pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function + + +@decorator1 +@decorator2 # fmt: skip +def bar(): + pass + + +# Regression test for https://github.com/psf/black/issues/3454. +def foo(): + pass + # Trailing comment that belongs to this function. + # NOTE this comment only has one empty line below, and the formatter + # should enforce two blank lines. + + +@decorator1 +# A standalone comment +def bar(): + pass From 3246df89d6d80fc09357b445630fad87f08f57ce Mon Sep 17 00:00:00 2001 From: Matthew Armand Date: Tue, 20 Dec 2022 22:18:15 -0500 Subject: [PATCH 387/700] Add latest_prerelease Docker Hub tag for following the latest alpha release (#3465) Co-authored-by: Jelle Zijlstra --- .github/workflows/docker.yml | 11 +++++++++++ CHANGES.md | 2 ++ docs/usage_and_configuration/black_docker_image.md | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 855186f9bf1..04e30e727bd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,5 +54,16 @@ jobs: push: true tags: pyfound/black:latest_release + - name: Build and push latest_prerelease tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + github.event.release.prerelease }} + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: pyfound/black:latest_prerelease + - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGES.md b/CHANGES.md index c07d81d1320..c89b10638ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,8 @@ - Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446) +- Docker: Add new `latest_prerelease` tag automation to follow latest black alpha + release on docker images (#3465) ### Documentation diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md index 8de566ea270..85aec91ef1c 100644 --- a/docs/usage_and_configuration/black_docker_image.md +++ b/docs/usage_and_configuration/black_docker_image.md @@ -10,6 +10,11 @@ _Black_ images with the following tags are available: - `latest_release` - tag created when a new version of _Black_ is released.\ ℹ Recommended for users who want to use released versions of _Black_. It maps to [the latest release](https://github.com/psf/black/releases/latest) of _Black_. +- `latest_prerelease` - tag created when a new alpha (prerelease) version of _Black_ is + released.\ + ℹ Recommended for users who want to preview or test alpha versions of _Black_. Note that + the most recent release may be newer than any prerelease, because no prereleases are created + before most releases. - `latest` - tag used for the newest image of _Black_.\ ℹ Recommended for users who always want to use the latest version of _Black_, even before it is released. From 3feff21eca0eee4b9fe72d12b506ac273cb5bcd0 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 23 Dec 2022 12:13:45 -0800 Subject: [PATCH 388/700] Significantly speedup ESP on large expressions that contain many strings (#3467) --- CHANGES.md | 1 + src/black/trans.py | 279 +++++++++++++++++++++-------- tests/data/preview/long_strings.py | 31 ++++ 3 files changed, 235 insertions(+), 76 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c89b10638ed..587ca8a2a0d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations and except clauses (#3423) diff --git a/src/black/trans.py b/src/black/trans.py index 0eb53e2b098..ec07f5eab74 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -69,7 +69,7 @@ class CannotTransform(Exception): ParserState = int StringID = int TResult = Result[T, CannotTransform] # (T)ransform Result -TMatchResult = TResult[Index] +TMatchResult = TResult[List[Index]] def TErr(err_msg: str) -> Err[CannotTransform]: @@ -198,14 +198,19 @@ def __init__(self, line_length: int, normalize_strings: bool) -> None: def do_match(self, line: Line) -> TMatchResult: """ Returns: - * Ok(string_idx) such that `line.leaves[string_idx]` is our target - string, if a match was able to be made. + * Ok(string_indices) such that for each index, `line.leaves[index]` + is our target string if a match was able to be made. For + transformers that don't result in more lines (e.g. StringMerger, + StringParenStripper), multiple matches and transforms are done at + once to reduce the complexity. OR - * Err(CannotTransform), if a match was not able to be made. + * Err(CannotTransform), if no match could be made. """ @abstractmethod - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: """ Yields: * Ok(new_line) where new_line is the new transformed line. @@ -246,9 +251,9 @@ def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line] " this line as one that it can transform." ) from cant_transform - string_idx = match_result.ok() + string_indices = match_result.ok() - for line_result in self.do_transform(line, string_idx): + for line_result in self.do_transform(line, string_indices): if isinstance(line_result, Err): cant_transform = line_result.err() raise CannotTransform( @@ -371,30 +376,50 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for i, leaf in enumerate(LL): + string_indices = [] + idx = 0 + while is_valid_index(idx): + leaf = LL[idx] if ( leaf.type == token.STRING - and is_valid_index(i + 1) - and LL[i + 1].type == token.STRING + and is_valid_index(idx + 1) + and LL[idx + 1].type == token.STRING ): - if is_part_of_annotation(leaf): - return TErr("String is part of type annotation.") - return Ok(i) + if not is_part_of_annotation(leaf): + string_indices.append(idx) - if leaf.type == token.STRING and "\\\n" in leaf.value: - return Ok(i) + # Advance to the next non-STRING leaf. + idx += 2 + while is_valid_index(idx) and LL[idx].type == token.STRING: + idx += 1 - return TErr("This line has no strings that need merging.") + elif leaf.type == token.STRING and "\\\n" in leaf.value: + string_indices.append(idx) + # Advance to the next non-STRING leaf. + idx += 1 + while is_valid_index(idx) and LL[idx].type == token.STRING: + idx += 1 - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + else: + idx += 1 + + if string_indices: + return Ok(string_indices) + else: + return TErr("This line has no strings that need merging.") + + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: new_line = line + rblc_result = self._remove_backslash_line_continuation_chars( - new_line, string_idx + new_line, string_indices ) if isinstance(rblc_result, Ok): new_line = rblc_result.ok() - msg_result = self._merge_string_group(new_line, string_idx) + msg_result = self._merge_string_group(new_line, string_indices) if isinstance(msg_result, Ok): new_line = msg_result.ok() @@ -415,7 +440,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: @staticmethod def _remove_backslash_line_continuation_chars( - line: Line, string_idx: int + line: Line, string_indices: List[int] ) -> TResult[Line]: """ Merge strings that were split across multiple lines using @@ -429,30 +454,40 @@ def _remove_backslash_line_continuation_chars( """ LL = line.leaves - string_leaf = LL[string_idx] - if not ( - string_leaf.type == token.STRING - and "\\\n" in string_leaf.value - and not has_triple_quotes(string_leaf.value) - ): + indices_to_transform = [] + for string_idx in string_indices: + string_leaf = LL[string_idx] + if ( + string_leaf.type == token.STRING + and "\\\n" in string_leaf.value + and not has_triple_quotes(string_leaf.value) + ): + indices_to_transform.append(string_idx) + + if not indices_to_transform: return TErr( - f"String leaf {string_leaf} does not contain any backslash line" - " continuation characters." + "Found no string leaves that contain backslash line continuation" + " characters." ) new_line = line.clone() new_line.comments = line.comments.copy() append_leaves(new_line, line, LL) - new_string_leaf = new_line.leaves[string_idx] - new_string_leaf.value = new_string_leaf.value.replace("\\\n", "") + for string_idx in indices_to_transform: + new_string_leaf = new_line.leaves[string_idx] + new_string_leaf.value = new_string_leaf.value.replace("\\\n", "") return Ok(new_line) - def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: + def _merge_string_group( + self, line: Line, string_indices: List[int] + ) -> TResult[Line]: """ - Merges string group (i.e. set of adjacent strings) where the first - string in the group is `line.leaves[string_idx]`. + Merges string groups (i.e. set of adjacent strings). + + Each index from `string_indices` designates one string group's first + leaf in `line.leaves`. Returns: Ok(new_line), if ALL of the validation checks found in @@ -464,10 +499,54 @@ def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: is_valid_index = is_valid_index_factory(LL) - vresult = self._validate_msg(line, string_idx) - if isinstance(vresult, Err): - return vresult + # A dict of {string_idx: tuple[num_of_strings, string_leaf]}. + merged_string_idx_dict: Dict[int, Tuple[int, Leaf]] = {} + for string_idx in string_indices: + vresult = self._validate_msg(line, string_idx) + if isinstance(vresult, Err): + continue + merged_string_idx_dict[string_idx] = self._merge_one_string_group( + LL, string_idx, is_valid_index + ) + + if not merged_string_idx_dict: + return TErr("No string group is merged") + + # Build the final line ('new_line') that this method will later return. + new_line = line.clone() + previous_merged_string_idx = -1 + previous_merged_num_of_strings = -1 + for i, leaf in enumerate(LL): + if i in merged_string_idx_dict: + previous_merged_string_idx = i + previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i] + new_line.append(string_leaf) + + if ( + previous_merged_string_idx + <= i + < previous_merged_string_idx + previous_merged_num_of_strings + ): + for comment_leaf in line.comments_after(LL[i]): + new_line.append(comment_leaf, preformatted=True) + continue + + append_leaves(new_line, line, [leaf]) + + return Ok(new_line) + def _merge_one_string_group( + self, LL: List[Leaf], string_idx: int, is_valid_index: Callable[[int], bool] + ) -> Tuple[int, Leaf]: + """ + Merges one string group where the first string in the group is + `LL[string_idx]`. + + Returns: + A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the + number of strings merged and `leaf` is the newly merged string + to be replaced in the new line. + """ # If the string group is wrapped inside an Atom node, we must make sure # to later replace that Atom with our new (merged) string leaf. atom_node = LL[string_idx].parent @@ -590,21 +669,8 @@ def make_naked(string: str, string_prefix: str) -> str: # Else replace the atom node with the new string leaf. replace_child(atom_node, string_leaf) - # Build the final line ('new_line') that this method will later return. - new_line = line.clone() - for i, leaf in enumerate(LL): - if i == string_idx: - new_line.append(string_leaf) - - if string_idx <= i < string_idx + num_of_strings: - for comment_leaf in line.comments_after(LL[i]): - new_line.append(comment_leaf, preformatted=True) - continue - - append_leaves(new_line, line, [leaf]) - self.add_custom_splits(string_leaf.value, custom_splits) - return Ok(new_line) + return num_of_strings, string_leaf @staticmethod def _validate_msg(line: Line, string_idx: int) -> TResult[None]: @@ -718,7 +784,15 @@ def do_match(self, line: Line) -> TMatchResult: is_valid_index = is_valid_index_factory(LL) - for idx, leaf in enumerate(LL): + string_indices = [] + + idx = -1 + while True: + idx += 1 + if idx >= len(LL): + break + leaf = LL[idx] + # Should be a string... if leaf.type != token.STRING: continue @@ -800,39 +874,73 @@ def do_match(self, line: Line) -> TMatchResult: }: continue - return Ok(string_idx) + string_indices.append(string_idx) + idx = string_idx + while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING: + idx += 1 + if string_indices: + return Ok(string_indices) return TErr("This line has no strings wrapped in parens.") - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves - string_parser = StringParser() - rpar_idx = string_parser.parse(LL, string_idx) + string_and_rpar_indices: List[int] = [] + for string_idx in string_indices: + string_parser = StringParser() + rpar_idx = string_parser.parse(LL, string_idx) + + should_transform = True + for leaf in (LL[string_idx - 1], LL[rpar_idx]): + if line.comments_after(leaf): + # Should not strip parentheses which have comments attached + # to them. + should_transform = False + break + if should_transform: + string_and_rpar_indices.extend((string_idx, rpar_idx)) - for leaf in (LL[string_idx - 1], LL[rpar_idx]): - if line.comments_after(leaf): - yield TErr( - "Will not strip parentheses which have comments attached to them." - ) - return + if string_and_rpar_indices: + yield Ok(self._transform_to_new_line(line, string_and_rpar_indices)) + else: + yield Err( + CannotTransform("All string groups have comments attached to them.") + ) + + def _transform_to_new_line( + self, line: Line, string_and_rpar_indices: List[int] + ) -> Line: + LL = line.leaves new_line = line.clone() new_line.comments = line.comments.copy() - append_leaves(new_line, line, LL[: string_idx - 1]) - string_leaf = Leaf(token.STRING, LL[string_idx].value) - LL[string_idx - 1].remove() - replace_child(LL[string_idx], string_leaf) - new_line.append(string_leaf) + previous_idx = -1 + # We need to sort the indices, since string_idx and its matching + # rpar_idx may not come in order, e.g. in + # `("outer" % ("inner".join(items)))`, the "inner" string's + # string_idx is smaller than "outer" string's rpar_idx. + for idx in sorted(string_and_rpar_indices): + leaf = LL[idx] + lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx + append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx]) + if leaf.type == token.STRING: + string_leaf = Leaf(token.STRING, LL[idx].value) + LL[lpar_or_rpar_idx].remove() # Remove lpar. + replace_child(LL[idx], string_leaf) + new_line.append(string_leaf) + else: + LL[lpar_or_rpar_idx].remove() # This is a rpar. - append_leaves( - new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] - ) + previous_idx = idx - LL[rpar_idx].remove() + # Append the leaves after the last idx: + append_leaves(new_line, line, LL[idx + 1 :]) - yield Ok(new_line) + return new_line class BaseStringSplitter(StringTransformer): @@ -885,7 +993,12 @@ def do_match(self, line: Line) -> TMatchResult: if isinstance(match_result, Err): return match_result - string_idx = match_result.ok() + string_indices = match_result.ok() + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] vresult = self._validate(line, string_idx) if isinstance(vresult, Err): return vresult @@ -1219,10 +1332,17 @@ def do_splitter_match(self, line: Line) -> TMatchResult: if is_valid_index(idx): return TErr("This line does not end with a string.") - return Ok(string_idx) + return Ok([string_idx]) - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] QUOTE = LL[string_idx].value[-1] @@ -1710,7 +1830,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult: " resultant line would still be over the specified line" " length and can't be split further by StringSplitter." ) - return Ok(string_idx) + return Ok([string_idx]) return TErr("This line does not contain any non-atomic strings.") @@ -1887,8 +2007,15 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: return None - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + def do_transform( + self, line: Line, string_indices: List[int] + ) -> Iterator[TResult[Line]]: LL = line.leaves + assert len(string_indices) == 1, ( + f"{self.__class__.__name__} should only find one match at a time, found" + f" {len(string_indices)}" + ) + string_idx = string_indices[0] is_valid_index = is_valid_index_factory(LL) insert_str_child = insert_str_child_factory(LL[string_idx]) diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 9c78f675b8f..b7a0a42f82a 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -287,6 +287,23 @@ def foo(): ), } +# Complex string concatenations with a method call in the middle. +code = ( + (" return [\n") + + ( + ", \n".join( + " (%r, self.%s, visitor.%s)" + % (attrname, attrname, visit_name) + for attrname, visit_name in names + ) + ) + + ("\n ]\n") +) + + +# Test case of an outer string' parens enclose an inner string's parens. +call(body=("%s %s" % ((",".join(items)), suffix))) + # output @@ -828,3 +845,17 @@ def foo(): f"{some_function_call(j.right)})" ), } + +# Complex string concatenations with a method call in the middle. +code = ( + " return [\n" + + ", \n".join( + " (%r, self.%s, visitor.%s)" % (attrname, attrname, visit_name) + for attrname, visit_name in names + ) + + "\n ]\n" +) + + +# Test case of an outer string' parens enclose an inner string's parens. +call(body="%s %s" % (",".join(items), suffix)) From 9b91638190342cf5a66d4edb11068526f7ebda59 Mon Sep 17 00:00:00 2001 From: Semen Zhydenko Date: Mon, 26 Dec 2022 03:39:51 +0100 Subject: [PATCH 389/700] Fix some typos (#3474) --- docs/contributing/issue_triage.md | 2 +- tests/data/preview/prefer_rhs_split.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 9b987fb2425..865a47935ed 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -42,7 +42,7 @@ The lifecycle of a bug report or user support issue typically goes something lik 1. _the issue is waiting for triage_ 2. **identified** - has been marked with a type label and other relevant labels, more details or a functional reproduction may be still needed (and therefore should be - marked with `S: needs repro` or `S: awaiting reponse`) + marked with `S: needs repro` or `S: awaiting response`) 3. **confirmed** - the issue can reproduced and necessary details have been provided 4. **discussion** - initial triage has been done and now the general details on how the issue should be best resolved are being hashed out diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py index 5b89113e618..2f3cf33db41 100644 --- a/tests/data/preview/prefer_rhs_split.py +++ b/tests/data/preview/prefer_rhs_split.py @@ -50,7 +50,7 @@ forth_item, fifth_item, last_item_very_loooooong, -) = everyting = some_loooooog_function_name( +) = everything = some_looooong_function_name( first_argument, second_argument, third_argument ) From 72a3408965f944f39f1080a5b67c25790acdc4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 06:32:42 -0800 Subject: [PATCH 390/700] Bump pypa/cibuildwheel from 2.11.3 to 2.11.4 (#3475) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.3 to 2.11.4. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.3...v2.11.4) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 7fd760ef727..ee1c1fa7bcf 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.3 + uses: pypa/cibuildwheel@v2.11.4 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 0abe85eebb94e7640aa5d443aefe5b9bed507bfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:00:23 -0800 Subject: [PATCH 391/700] Bump peter-evans/find-comment from 2.1.0 to 2.2.0 (#3476) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/f4499a714d59013c74a08789b48abe4b704364a0...81e2da3af01c92f83cb927cf3ace0e085617c556) --- updated-dependencies: - dependency-name: peter-evans/find-comment dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 1375be9788d..26d06090919 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 + uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From 4e3303fa08e030722d6fd4d7fe7b8d44ef98991c Mon Sep 17 00:00:00 2001 From: Jordan Ephron Date: Thu, 29 Dec 2022 18:13:15 -0500 Subject: [PATCH 392/700] Parenthesize conditional expressions (#2278) Co-authored-by: Jordan Ephron Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/__init__.py | 16 ++- src/black/linegen.py | 16 +++ src/black/mode.py | 1 + tests/data/conditional_expression.py | 160 +++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 tests/data/conditional_expression.py diff --git a/CHANGES.md b/CHANGES.md index 587ca8a2a0d..2da0fb4720c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Add parentheses around `if`-`else` expressions (#2278) - Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations diff --git a/src/black/__init__.py b/src/black/__init__.py index f00749aaed8..9f44722bfae 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -478,16 +478,20 @@ def main( # noqa: C901 ) normalized = [ - (source, source) - if source == "-" - else (normalize_path_maybe_ignore(Path(source), root), source) + ( + (source, source) + if source == "-" + else (normalize_path_maybe_ignore(Path(source), root), source) + ) for source in src ] srcs_string = ", ".join( [ - f'"{_norm}"' - if _norm - else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ( + f'"{_norm}"' + if _norm + else f'\033[31m"{source} (skipping - invalid)"\033[34m' + ) for _norm, source in normalized ] ) diff --git a/src/black/linegen.py b/src/black/linegen.py index 2e75bc94506..4da75b28235 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -140,6 +140,22 @@ def visit_default(self, node: LN) -> Iterator[Line]: self.current_line.append(node) yield from super().visit_default(node) + def visit_test(self, node: Node) -> Iterator[Line]: + """Visit an `x if y else z` test""" + + if Preview.parenthesize_conditional_expressions in self.mode: + already_parenthesized = ( + node.prev_sibling and node.prev_sibling.type == token.LPAR + ) + + if not already_parenthesized: + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + node.insert_child(0, lpar) + node.append_child(rpar) + + yield from self.visit_default(node) + def visit_INDENT(self, node: Leaf) -> Iterator[Line]: """Increase indentation level, maybe yield a line.""" # In blib2to3 INDENT never holds comments. diff --git a/src/black/mode.py b/src/black/mode.py index a104d1b9862..775805ae960 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -161,6 +161,7 @@ class Preview(Enum): # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() + parenthesize_conditional_expressions = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() diff --git a/tests/data/conditional_expression.py b/tests/data/conditional_expression.py new file mode 100644 index 00000000000..620a12dc986 --- /dev/null +++ b/tests/data/conditional_expression.py @@ -0,0 +1,160 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) + +# output + +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) From 37542e64855ce21bd580f973ae5ce1ed86812a7a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 31 Dec 2022 01:52:35 -0500 Subject: [PATCH 393/700] Fail lint CI if the PR doesn't target main (#3477) Let's skip the check if we're running on a fork just in case someone opens a PR against a branch on said fork as part of a PR review upstream. --- .github/workflows/lint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90c48013080..064d4745a53 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,13 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Assert PR target is main + if: github.event_name == 'pull_request' && github.repository == 'psf/black' + run: | + if [ "$GITHUB_BASE_REF" != "main" ]; then + echo "::error::PR targeting '$GITHUB_BASE_REF', please refile targeting 'main'." && exit 1 + fi + - name: Set up latest Python uses: actions/setup-python@v4 with: From 5d0d5936db2ed7a01c50a374e32753e1afe9cc71 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 2 Jan 2023 09:43:48 -0500 Subject: [PATCH 394/700] Add email for Richard Si (#3478) --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index a635e8c3c92..ab3f30b8821 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -10,7 +10,7 @@ Maintained with: - [Mika Naylor](mailto:mail@autophagy.io) - [Zsolt Dollenstein](mailto:zsol.zsol@gmail.com) - [Cooper Lees](mailto:me@cooperlees.com) -- Richard Si +- [Richard Si](mailto:sichard26@gmail.com) - [Felix Hildén](mailto:felix.hilden@gmail.com) - [Batuhan Taskaya](mailto:batuhan@python.org) From 4bee9cca5553c55493203822b5a112ec5216bc74 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Jan 2023 16:19:27 -0300 Subject: [PATCH 395/700] Remove misleading phrase in Usage and Configuration (#3492) The CLI options were already shown in the "Command line options" in the same page. --- docs/usage_and_configuration/the_basics.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 3dab644f2c8..9dc5277c61e 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -40,6 +40,9 @@ so style options are deliberately limited and rarely added.
+Note that all command-line options listed above can also be configured using a +`pyproject.toml` file (more on that below). + ### Code input alternatives #### Standard Input @@ -287,9 +290,6 @@ file hierarchy. ## Next steps -You've probably noted that not all of the options you can pass to _Black_ have been -covered. Don't worry, the rest will be covered in a later section. - A good next step would be configuring auto-discovery so `black .` is all you need instead of laborously listing every file or directory. You can get started by heading over to [File collection and discovery](./file_collection_and_discovery.md). From f7580103407743a317e22297793822dd91f8fefe Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sat, 14 Jan 2023 09:51:59 -0800 Subject: [PATCH 396/700] Documentation: clarify the state of multiple context managers (#3488) Clarify that the backslash & paren-wrapping formatting for multiple context managers aren't yet implemented. --- docs/the_black_code_style/future_style.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 17b7eef092f..9ca260fc0ad 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -19,7 +19,7 @@ with make_context_manager1() as cm1, make_context_manager2() as cm2, make_contex ... # nothing to split on - line too long ``` -So _Black_ will eventually format it like this: +So _Black_ will, when we implement this, format it like this: ```py3 with \ @@ -31,8 +31,8 @@ with \ ... # backslashes and an ugly stranded colon ``` -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. +Although when the target version is Python 3.9 or higher, _Black_ will, when we +implement this, use parentheses instead since they're allowed in Python 3.9 and higher. An alternative to consider if the backslashes in the above formatting are undesirable is to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the From d4ff985853c8d140d73b9d362604deedb41eb20e Mon Sep 17 00:00:00 2001 From: Ruslan <7631314+ruslaniv@users.noreply.github.com> Date: Sun, 15 Jan 2023 01:32:00 +0700 Subject: [PATCH 397/700] Add IntelliJ docs on external tools and file watcher (#3365) Revert deleted documentation on setting up Black using IntelliJ external tool or file watcher utilities. These are still worth keeping because some peole might not want to use a third-party plugin or install Blackd's extra dependencies. Co-authored-by: Richard Si --- docs/integrations/editors.md | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index a8b7978c4d7..74c6a283ab8 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -10,6 +10,19 @@ Options include the following: ## PyCharm/IntelliJ IDEA +There are three different ways you can use _Black_ from PyCharm: + +1. As local server using the BlackConnect plugin +1. As external tool +1. As file watcher + +The first option is the simplest to set up and formats the fastest (by spinning up +{doc}`Black's HTTP server `, avoiding the +startup cost on subsequent formats), but if you would prefer to not install a +third-party plugin or blackd's extra dependencies, the other two are also great options. + +### As local server + 1. Install _Black_ with the `d` extra. ```console @@ -46,6 +59,99 @@ Options include the following: - In `Trigger Settings` section of plugin configuration check `Trigger when saving changed files`. +### As external tool + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Open External tools in PyCharm/IntelliJ IDEA + + On macOS: + + `PyCharm -> Preferences -> Tools -> External Tools` + + On Windows / Linux / BSD: + + `File -> Settings -> Tools -> External Tools` + +1. Click the + icon to add a new external tool with the following values: + + - Name: Black + - Description: Black is the uncompromising Python code formatter. + - Program: \ + - Arguments: `"$FilePath$"` + +1. Format the currently opened file by selecting `Tools -> External Tools -> black`. + + - Alternatively, you can set a keyboard shortcut by navigating to + `Preferences or Settings -> Keymap -> External Tools -> External Tools - Black`. + +### As file watcher + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Locate your `black` installation folder. + + On macOS / Linux / BSD: + + ```console + $ which black + /usr/local/bin/black # possible location + ``` + + On Windows: + + ```console + $ where black + %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + ``` + + Note that if you are using a virtual environment detected by PyCharm, this is an + unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`. + +1. Make sure you have the + [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin + installed. +1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a new + watcher: + - Name: Black + - File type: Python + - Scope: Project Files + - Program: \ + - Arguments: `$FilePath$` + - Output paths to refresh: `$FilePath$` + - Working directory: `$ProjectFileDir$` + +- In Advanced Options + - Uncheck "Auto-save edited files to trigger the watcher" + - Uncheck "Trigger the watcher on external changes" + ## Wing IDE Wing IDE supports `black` via **Preference Settings** for system wide settings and From 60a2e8e2c26d6312cd86b40f680c5037571acafc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 16 Jan 2023 12:26:03 -0800 Subject: [PATCH 398/700] Fix two docstring crashes (#3451) --- CHANGES.md | 1 + src/black/linegen.py | 4 ++++ tests/data/miscellaneous/linelength6.py | 5 +++++ tests/data/simple_cases/docstring.py | 11 +++++++++++ tests/test_format.py | 7 +++++++ 5 files changed, 28 insertions(+) create mode 100644 tests/data/miscellaneous/linelength6.py diff --git a/CHANGES.md b/CHANGES.md index 2da0fb4720c..17dc0d686df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ - Long values in dict literals are now wrapped in parentheses; correspondingly unnecessary parentheses around short values in dict literals are now removed; long string lambda values are now wrapped in parentheses (#3440) +- Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4da75b28235..da41886f80d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -401,6 +401,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: else: docstring = docstring.strip() + has_trailing_backslash = False if docstring: # Add some padding if the docstring starts / ends with a quote mark. if docstring[0] == quote_char: @@ -413,6 +414,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # Odd number of tailing backslashes, add some padding to # avoid escaping the closing string quote. docstring += " " + has_trailing_backslash = True elif not docstring_started_empty: docstring = " " @@ -435,6 +437,8 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if ( len(lines) > 1 and last_line_length + quote_len > self.mode.line_length + and len(indent) + quote_len <= self.mode.line_length + and not has_trailing_backslash ): leaf.value = prefix + quote + docstring + "\n" + indent + quote else: diff --git a/tests/data/miscellaneous/linelength6.py b/tests/data/miscellaneous/linelength6.py new file mode 100644 index 00000000000..4fb342726f5 --- /dev/null +++ b/tests/data/miscellaneous/linelength6.py @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/tests/data/simple_cases/docstring.py b/tests/data/simple_cases/docstring.py index f08bba575fe..c31d6a68783 100644 --- a/tests/data/simple_cases/docstring.py +++ b/tests/data/simple_cases/docstring.py @@ -173,6 +173,11 @@ def multiline_backslash_2(): ''' hey there \ ''' +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + def multiline_backslash_3(): ''' @@ -391,6 +396,12 @@ def multiline_backslash_2(): hey there \ """ +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + def multiline_backslash_3(): """ already escaped \\""" diff --git a/tests/test_format.py b/tests/test_format.py index 01cd61eef63..0816bbd3692 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -146,6 +146,13 @@ def test_docstring_no_string_normalization() -> None: assert_format(source, expected, mode) +def test_docstring_line_length_6() -> None: + """Like test_docstring but with line length set to 6.""" + source, expected = read_data("miscellaneous", "linelength6") + mode = black.Mode(line_length=6) + assert_format(source, expected, mode) + + def test_preview_docstring_no_string_normalization() -> None: """ Like test_docstring but with string normalization off *and* the preview style From 24469c9bd14c3ddb4739f24c661aeb725f2decd5 Mon Sep 17 00:00:00 2001 From: Bartosz Sokorski Date: Wed, 18 Jan 2023 03:01:03 +0100 Subject: [PATCH 399/700] Add flake8-bugbear B907 to ignore list (#3503) --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index eddaaba81fd..7bc346a09c1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # B905 should be enabled when we drop support for 3.9 -ignore = E203, E266, E501, W503, B905 +ignore = E203, E266, E501, W503, B905, B907 # line length is intentionally set to 80 here because black uses Bugbear # See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details max-line-length = 80 From 7e6d3fac197395b0a2b380cc60811536fe23626b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 17 Jan 2023 22:25:05 -0800 Subject: [PATCH 400/700] Fix crash with walrus + await + with (#3473) Fixes #3472 --- CHANGES.md | 2 + src/black/linegen.py | 4 ++ src/black/nodes.py | 11 +++++ .../data/fast/pep_572_do_not_remove_parens.py | 4 ++ tests/data/py_38/pep_572_remove_parens.py | 40 +++++++++++++++++++ 5 files changed, 61 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 17dc0d686df..97b68b90fb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,8 @@ - Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) +- Fix several crashes in preview style with walrus operators used in `with` statements + or tuples (#3473) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index da41886f80d..14f851161fd 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -46,6 +46,7 @@ is_rpar_token, is_stub_body, is_stub_suite, + is_tuple_containing_walrus, is_vararg, is_walrus_assignment, is_yield, @@ -1279,6 +1280,7 @@ def maybe_make_parens_invisible_in_atom( not remove_brackets_around_comma and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY ) + or is_tuple_containing_walrus(node) ): return False @@ -1290,9 +1292,11 @@ def maybe_make_parens_invisible_in_atom( syms.return_stmt, syms.except_clause, syms.funcdef, + syms.with_stmt, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, + syms.for_stmt, ]: return False diff --git a/src/black/nodes.py b/src/black/nodes.py index a11fb7cc071..a588077f4de 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -563,6 +563,17 @@ def is_one_tuple(node: LN) -> bool: ) +def is_tuple_containing_walrus(node: LN) -> bool: + """Return True if `node` holds a tuple that contains a walrus operator.""" + if node.type != syms.atom: + return False + gexp = unwrap_singleton_parenthesis(node) + if gexp is None or gexp.type != syms.testlist_gexp: + return False + + return any(child.type == syms.namedexpr_test for child in gexp.children) + + def is_one_sequence_between( opening: Leaf, closing: Leaf, diff --git a/tests/data/fast/pep_572_do_not_remove_parens.py b/tests/data/fast/pep_572_do_not_remove_parens.py index 20e80a69377..05619ddcc2b 100644 --- a/tests/data/fast/pep_572_do_not_remove_parens.py +++ b/tests/data/fast/pep_572_do_not_remove_parens.py @@ -19,3 +19,7 @@ @(please := stop) def sigh(): pass + + +for (x := 3, y := 4) in y: + pass diff --git a/tests/data/py_38/pep_572_remove_parens.py b/tests/data/py_38/pep_572_remove_parens.py index 9718d95b499..4e95fb07f3a 100644 --- a/tests/data/py_38/pep_572_remove_parens.py +++ b/tests/data/py_38/pep_572_remove_parens.py @@ -49,6 +49,26 @@ def a(): def this_is_so_dumb() -> (please := no): pass +async def await_the_walrus(): + with (x := y): + pass + + with (x := y) as z, (a := b) as c: + pass + + with (x := await y): + pass + + with (x := await a, y := await b): + pass + + # Ideally we should remove one set of parentheses + with ((x := await a, y := await b)): + pass + + with (x := await a), (y := await b): + pass + # output if foo := 0: @@ -103,3 +123,23 @@ def a(): def this_is_so_dumb() -> (please := no): pass + +async def await_the_walrus(): + with (x := y): + pass + + with (x := y) as z, (a := b) as c: + pass + + with (x := await y): + pass + + with (x := await a, y := await b): + pass + + # Ideally we should remove one set of parentheses + with ((x := await a, y := await b)): + pass + + with (x := await a), (y := await b): + pass From 18fb88486d434dbde9b2a9c98f008a71cf5d941d Mon Sep 17 00:00:00 2001 From: Antonio Ossa-Guerra Date: Wed, 18 Jan 2023 23:38:27 -0300 Subject: [PATCH 401/700] Fix false symlink detection claims in verbose output (#3385) When trying to format a project from the outside, the verbose output shows says that there are symbolic links that points outside of the project, but displays the wrong project path, meaning that these messages are false positives. This bug is triggered when the command is executed from outside a project on a folder inside it, causing an inconsistency between the path to the detected project root and the relative path to the target contents. The fix is to normalize the target path using the project root before processing the sources, which removes the presence of the incorrect messages. --- The test attemps to emulate the behavior of the CLI as closely as posible by patching some `pathlib.Path` methods and passing certain reference paths to the context object and `black.get_sources`. Before the associated fix was introduced, this test failed because some of the captured files reported the presence of a symlink due to an incorrectly formated path. The test also asserts that only a single file is reported as ignored, which is part of the expected behavior. Signed-off-by: Antonio Ossa Guerra --- CHANGES.md | 2 ++ src/black/__init__.py | 3 ++- tests/test_black.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 97b68b90fb1..313536e8480 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -66,6 +66,8 @@ - Verbose logging now shows the values of `pyproject.toml` configuration variables (#3392) +- Fix false symlink detection messages in verbose output due to using an incorrect + relative path to the project root (#3385) ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 9f44722bfae..5d35c805bac 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -673,10 +673,11 @@ def get_sources( sources.add(p) elif p.is_dir(): + p = root / normalize_path_maybe_ignore(p, ctx.obj["root"], report) if using_default_exclude: gitignore = { root: root_gitignore, - root / p: get_gitignore(p), + p: get_gitignore(p), } sources.update( gen_python_files( diff --git a/tests/test_black.py b/tests/test_black.py index dda10555c97..44d617244f1 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -475,6 +475,53 @@ def test_tab_comment_indentation(self) -> None: self.assertFormatEqual(contents_spc, fs(contents_spc)) self.assertFormatEqual(contents_spc, fs(contents_tab)) + def test_false_positive_symlink_output_issue_3384(self) -> None: + # Emulate the behavior when using the CLI (`black ./child --verbose`), which + # involves patching some `pathlib.Path` methods. In particular, `is_dir` is + # patched only on its first call: when checking if "./child" is a directory it + # should return True. The "./child" folder exists relative to the cwd when + # running from CLI, but fails when running the tests because cwd is different + project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests") + working_directory = project_root / "root" + target_abspath = working_directory / "child" + target_contents = ( + src.relative_to(working_directory) for src in target_abspath.iterdir() + ) + + def mock_n_calls(responses: List[bool]) -> Callable[[], bool]: + def _mocked_calls() -> bool: + if responses: + return responses.pop(0) + return False + + return _mocked_calls + + with patch("pathlib.Path.iterdir", return_value=target_contents), patch( + "pathlib.Path.cwd", return_value=working_directory + ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])): + ctx = FakeContext() + ctx.obj["root"] = project_root + report = MagicMock(verbose=True) + black.get_sources( + ctx=ctx, + src=("./child",), + quiet=False, + verbose=True, + include=DEFAULT_INCLUDE, + exclude=None, + report=report, + extend_exclude=None, + force_exclude=None, + stdin_filename=None, + ) + assert not any( + mock_args[1].startswith("is a symbolic link that points outside") + for _, mock_args, _ in report.path_ignored.mock_calls + ), "A symbolic link was reported." + report.path_ignored.assert_called_once_with( + Path("child", "b.py"), "matches a .gitignore file content" + ) + def test_report_verbose(self) -> None: report = Report(verbose=True) out_lines = [] From 91e1e1328aa0a11ef50017316ff97149886e1b05 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 20 Jan 2023 04:14:05 -0800 Subject: [PATCH 402/700] Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489) --- CHANGES.md | 1 + src/black/__init__.py | 28 ++++- src/black/linegen.py | 101 +++++++++++++---- src/black/mode.py | 5 + .../auto_detect/features_3_10.py | 35 ++++++ .../auto_detect/features_3_11.py | 37 +++++++ .../auto_detect/features_3_8.py | 30 +++++ .../auto_detect/features_3_9.py | 34 ++++++ .../targeting_py38.py | 38 +++++++ .../targeting_py39.py | 104 ++++++++++++++++++ tests/test_format.py | 24 ++++ 11 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_10.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_11.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_8.py create mode 100644 tests/data/preview_context_managers/auto_detect/features_3_9.py create mode 100644 tests/data/preview_context_managers/targeting_py38.py create mode 100644 tests/data/preview_context_managers/targeting_py39.py diff --git a/CHANGES.md b/CHANGES.md index 313536e8480..1450278341b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,7 @@ - Fix two crashes in preview style involving edge cases with docstrings (#3451) - Exclude string type annotations from improved string processing; fix crash when the return type annotation is stringified and spans across multiple lines (#3462) +- Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489) - Fix several crashes in preview style with walrus operators used in `with` statements or tuples (#3473) diff --git a/src/black/__init__.py b/src/black/__init__.py index 5d35c805bac..daf6f88f58e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1096,8 +1096,13 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: future_imports = get_future_imports(src_node) versions = detect_target_versions(src_node, future_imports=future_imports) + context_manager_features = { + feature + for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + if supports_feature(versions, feature) + } normalize_fmt_off(src_node, preview=mode.preview) - lines = LineGenerator(mode=mode) + lines = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { feature @@ -1159,6 +1164,10 @@ def get_features_used( # noqa: C901 - relaxed decorator syntax; - usage of __future__ flags (annotations); - print / exec statements; + - parenthesized context managers; + - match statements; + - except* clause; + - variadic generics; """ features: Set[Feature] = set() if future_imports: @@ -1234,6 +1243,23 @@ def get_features_used( # noqa: C901 ): features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) + elif ( + n.type == syms.with_stmt + and len(n.children) > 2 + and n.children[1].type == syms.atom + ): + atom_children = n.children[1].children + if ( + len(atom_children) == 3 + and atom_children[0].type == token.LPAR + and atom_children[1].type == syms.testlist_gexp + and atom_children[2].type == token.RPAR + ): + features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) + + elif n.type == syms.match_stmt: + features.add(Feature.PATTERN_MATCHING) + elif ( n.type == syms.except_clause and len(n.children) >= 2 diff --git a/src/black/linegen.py b/src/black/linegen.py index 14f851161fd..2f50257a930 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -90,8 +90,9 @@ class LineGenerator(Visitor[Line]): in ways that will no longer stringify to valid Python code on the tree. """ - def __init__(self, mode: Mode) -> None: + def __init__(self, mode: Mode, features: Collection[Feature]) -> None: self.mode = mode + self.features = features self.current_line: Line self.__post_init__() @@ -191,7 +192,9 @@ def visit_stmt( `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens, preview=self.mode.preview) + normalize_invisible_parens( + node, parens_after=parens, mode=self.mode, features=self.features + ) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() @@ -244,7 +247,9 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]: def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) + normalize_invisible_parens( + node, parens_after=set(), mode=self.mode, features=self.features + ) yield from self.line() for child in node.children: @@ -1090,7 +1095,7 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: def normalize_invisible_parens( - node: Node, parens_after: Set[str], *, preview: bool + node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: """Make existing optional parentheses invisible or create new ones. @@ -1100,17 +1105,24 @@ def normalize_invisible_parens( Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. """ - for pc in list_comments(node.prefix, is_endmarker=False, preview=preview): + for pc in list_comments(node.prefix, is_endmarker=False, preview=mode.preview): if pc.value in FMT_OFF: # This `node` has a prefix with `# fmt: off`, don't mess with parens. return + + # The multiple context managers grammar has a different pattern, thus this is + # separate from the for-loop below. This possibly wraps them in invisible parens, + # and later will be removed in remove_with_parens when needed. + if node.type == syms.with_stmt: + _maybe_wrap_cms_in_parens(node, mode, features) + check_lpar = False for index, child in enumerate(list(node.children)): # Fixes a bug where invisible parens are not properly stripped from # assignment statements that contain type annotations. if isinstance(child, Node) and child.type == syms.annassign: normalize_invisible_parens( - child, parens_after=parens_after, preview=preview + child, parens_after=parens_after, mode=mode, features=features ) # Add parentheses around long tuple unpacking in assignments. @@ -1123,7 +1135,7 @@ def normalize_invisible_parens( if check_lpar: if ( - preview + mode.preview and child.type == syms.atom and node.type == syms.for_stmt and isinstance(child.prev_sibling, Leaf) @@ -1136,7 +1148,9 @@ def normalize_invisible_parens( remove_brackets_around_comma=True, ): wrap_in_parentheses(node, child, visible=False) - elif preview and isinstance(child, Node) and node.type == syms.with_stmt: + elif ( + mode.preview and isinstance(child, Node) and node.type == syms.with_stmt + ): remove_with_parens(child, node) elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom( @@ -1147,17 +1161,7 @@ def normalize_invisible_parens( elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) elif node.type == syms.import_from: - # "import from" nodes store parentheses directly as part of - # the statement - if is_lpar_token(child): - assert is_rpar_token(node.children[-1]) - # make parentheses invisible - child.value = "" - node.children[-1].value = "" - elif child.type != token.STAR: - # insert invisible parentheses - node.insert_child(index, Leaf(token.LPAR, "")) - node.append_child(Leaf(token.RPAR, "")) + _normalize_import_from(node, child, index) break elif ( index == 1 @@ -1172,13 +1176,27 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - comma_check = child.type == token.COMMA if preview else False + comma_check = child.type == token.COMMA if mode.preview else False check_lpar = isinstance(child, Leaf) and ( child.value in parens_after or comma_check ) +def _normalize_import_from(parent: Node, child: LN, index: int) -> None: + # "import from" nodes store parentheses directly as part of + # the statement + if is_lpar_token(child): + assert is_rpar_token(parent.children[-1]) + # make parentheses invisible + child.value = "" + parent.children[-1].value = "" + elif child.type != token.STAR: + # insert invisible parentheses + parent.insert_child(index, Leaf(token.LPAR, "")) + parent.append_child(Leaf(token.RPAR, "")) + + def remove_await_parens(node: Node) -> None: if node.children[0].type == token.AWAIT and len(node.children) > 1: if ( @@ -1215,6 +1233,49 @@ def remove_await_parens(node: Node) -> None: remove_await_parens(bracket_contents) +def _maybe_wrap_cms_in_parens( + node: Node, mode: Mode, features: Collection[Feature] +) -> None: + """When enabled and safe, wrap the multiple context managers in invisible parens. + + It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS. + """ + if ( + Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features + or Preview.wrap_multiple_context_managers_in_parens not in mode + or len(node.children) <= 2 + # If it's an atom, it's already wrapped in parens. + or node.children[1].type == syms.atom + ): + return + colon_index: Optional[int] = None + for i in range(2, len(node.children)): + if node.children[i].type == token.COLON: + colon_index = i + break + if colon_index is not None: + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + context_managers = node.children[1:colon_index] + for child in context_managers: + child.remove() + # After wrapping, the with_stmt will look like this: + # with_stmt + # NAME 'with' + # atom + # LPAR '' + # testlist_gexp + # ... <-- context_managers + # /testlist_gexp + # RPAR '' + # /atom + # COLON ':' + new_child = Node( + syms.atom, [lpar, Node(syms.testlist_gexp, context_managers), rpar] + ) + node.insert_child(1, new_child) + + def remove_with_parens(node: Node, parent: Node) -> None: """Recursively hide optional parens in `with` statements.""" # Removing all unnecessary parentheses in with statements in one pass is a tad diff --git a/src/black/mode.py b/src/black/mode.py index 775805ae960..af0706e6a0b 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -50,6 +50,7 @@ class Feature(Enum): EXCEPT_STAR = 14 VARIADIC_GENERICS = 15 DEBUG_F_STRINGS = 16 + PARENTHESIZED_CONTEXT_MANAGERS = 17 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -106,6 +107,7 @@ class Feature(Enum): Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, }, TargetVersion.PY310: { Feature.F_STRINGS, @@ -120,6 +122,7 @@ class Feature(Enum): Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, Feature.PATTERN_MATCHING, }, TargetVersion.PY311: { @@ -135,6 +138,7 @@ class Feature(Enum): Feature.POS_ONLY_ARGUMENTS, Feature.UNPACKING_ON_FLOW, Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, Feature.PATTERN_MATCHING, Feature.EXCEPT_STAR, Feature.VARIADIC_GENERICS, @@ -164,6 +168,7 @@ class Preview(Enum): parenthesize_conditional_expressions = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() + wrap_multiple_context_managers_in_parens = auto() class Deprecated(UserWarning): diff --git a/tests/data/preview_context_managers/auto_detect/features_3_10.py b/tests/data/preview_context_managers/auto_detect/features_3_10.py new file mode 100644 index 00000000000..1458df1cb41 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_10.py @@ -0,0 +1,35 @@ +# This file uses pattern matching introduced in Python 3.10. + + +match http_code: + case 404: + print("Not found") + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output + + +# This file uses pattern matching introduced in Python 3.10. + + +match http_code: + case 404: + print("Not found") + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_11.py b/tests/data/preview_context_managers/auto_detect/features_3_11.py new file mode 100644 index 00000000000..f83c5330ab3 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_11.py @@ -0,0 +1,37 @@ +# This file uses except* clause in Python 3.11. + + +try: + some_call() +except* Error as e: + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output + + +# This file uses except* clause in Python 3.11. + + +try: + some_call() +except* Error as e: + pass + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_8.py b/tests/data/preview_context_managers/auto_detect/features_3_8.py new file mode 100644 index 00000000000..e05094e1421 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_8.py @@ -0,0 +1,30 @@ +# This file doesn't use any Python 3.9+ only grammars. + + +# Make sure parens around a single context manager don't get autodetected as +# Python 3.9+. +with (a): + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# output +# This file doesn't use any Python 3.9+ only grammars. + + +# Make sure parens around a single context manager don't get autodetected as +# Python 3.9+. +with a: + pass + + +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + pass diff --git a/tests/data/preview_context_managers/auto_detect/features_3_9.py b/tests/data/preview_context_managers/auto_detect/features_3_9.py new file mode 100644 index 00000000000..0d28f993108 --- /dev/null +++ b/tests/data/preview_context_managers/auto_detect/features_3_9.py @@ -0,0 +1,34 @@ +# This file uses parenthesized context managers introduced in Python 3.9. + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +with ( + new_new_new1() as cm1, + new_new_new2() +): + pass + + +# output +# This file uses parenthesized context managers introduced in Python 3.9. + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass diff --git a/tests/data/preview_context_managers/targeting_py38.py b/tests/data/preview_context_managers/targeting_py38.py new file mode 100644 index 00000000000..6ec4684e441 --- /dev/null +++ b/tests/data/preview_context_managers/targeting_py38.py @@ -0,0 +1,38 @@ +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +with \ + make_context_manager1() as cm1, \ + make_context_manager2(), \ + make_context_manager3() as cm3, \ + make_context_manager4() \ +: + pass + + +with \ + new_new_new1() as cm1, \ + new_new_new2() \ +: + pass + + +# output + + +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + pass + + +with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4(): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py new file mode 100644 index 00000000000..5cb8763040a --- /dev/null +++ b/tests/data/preview_context_managers/targeting_py39.py @@ -0,0 +1,104 @@ +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + pass + + +# Leading comment +with \ + make_context_manager1() as cm1, \ + make_context_manager2(), \ + make_context_manager3() as cm3, \ + make_context_manager4() \ +: + pass + + +with \ + new_new_new1() as cm1, \ + new_new_new2() \ +: + pass + + +with ( + new_new_new1() as cm1, + new_new_new2() +): + pass + + +# Leading comment. +with ( + # First comment. + new_new_new1() as cm1, + # Second comment. + new_new_new2() + # Last comment. +): + pass + + +with \ + this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2) as cm1, \ + this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2, looong_arg3=looong_value3, looong_arg4=looong_value4) as cm2 \ +: + pass + + +# output + + +with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, +): + pass + + +# Leading comment +with ( + make_context_manager1() as cm1, + make_context_manager2(), + make_context_manager3() as cm3, + make_context_manager4(), +): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass + + +with new_new_new1() as cm1, new_new_new2(): + pass + + +# Leading comment. +with ( + # First comment. + new_new_new1() as cm1, + # Second comment. + new_new_new2() + # Last comment. +): + pass + + +with ( + this_is_a_very_long_call( + looong_arg1=looong_value1, looong_arg2=looong_value2 + ) as cm1, + this_is_a_very_long_call( + looong_arg1=looong_value1, + looong_arg2=looong_value2, + looong_arg3=looong_value3, + looong_arg4=looong_value4, + ) as cm2, +): + pass diff --git a/tests/test_format.py b/tests/test_format.py index 0816bbd3692..adcbc02468d 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,3 +1,4 @@ +import re from dataclasses import replace from typing import Any, Iterator from unittest.mock import patch @@ -58,6 +59,29 @@ def test_preview_minimum_python_310_format(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 10)) +def test_preview_context_managers_targeting_py38() -> None: + source, expected = read_data("preview_context_managers", "targeting_py38.py") + mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY38}) + assert_format(source, expected, mode, minimum_version=(3, 8)) + + +def test_preview_context_managers_targeting_py39() -> None: + source, expected = read_data("preview_context_managers", "targeting_py39.py") + mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY39}) + assert_format(source, expected, mode, minimum_version=(3, 9)) + + +@pytest.mark.parametrize( + "filename", all_data_cases("preview_context_managers/auto_detect") +) +def test_preview_context_managers_auto_detect(filename: str) -> None: + match = re.match(r"features_3_(\d+)", filename) + assert match is not None, "Unexpected filename format: %s" % filename + source, expected = read_data("preview_context_managers/auto_detect", filename) + mode = black.Mode(preview=True) + assert_format(source, expected, mode, minimum_version=(3, int(match.group(1)))) + + # =============== # # Complex cases # ============= # From c5df7b7d3ca5b568a81a9ccc1f647c183e8b4075 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 21 Jan 2023 02:50:00 -0500 Subject: [PATCH 403/700] Reenable macOS mypyc wheel build (#3511) Hatchling implemented a workaround for the 'technically right tag but no one understands it, including pip' issue so this should work now. --- .github/workflows/pypi_upload.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index ee1c1fa7bcf..6b3eb903d84 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -47,13 +47,12 @@ jobs: - os: macos-11 name: macos-x86_64 macos_arch: "x86_64" - # Only build x86_64 wheels on macos until #3312 is fixed - # - os: macos-11 - # name: macos-arm64 - # macos_arch: "arm64" - # - os: macos-11 - # name: macos-universal2 - # macos_arch: "universal2" + - os: macos-11 + name: macos-arm64 + macos_arch: "arm64" + - os: macos-11 + name: macos-universal2 + macos_arch: "universal2" steps: - uses: actions/checkout@v3 From 1557f7d3a380ed38801f5fada27550c10f89870f Mon Sep 17 00:00:00 2001 From: Michael Eliachevitch Date: Sun, 22 Jan 2023 06:20:54 +0100 Subject: [PATCH 404/700] Use dashes for pycodestyle max line length config (#3513) The option is `max-line-length` with dashes, not underscores. The config option name is given in the output of `pycodestyle -h`, which can also be checked on https://pep8.readthedocs.io/en/stable/intro.html#example-usage-and-output: ``` Configuration: The project options are read from the [pycodestyle] section of the tox.ini file or the setup.cfg file located in any parent folder of the path(s) being processed. Allowed options are: exclude, filename, select, ignore, max-line-length, max-doc-length, hang-closing, count, format, quiet, show-pep8, show-source, statistics, verbose ``` --- docs/guides/using_black_with_other_tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 7bc0726dc27..9356caaf0bd 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -260,7 +260,7 @@ max-line-length = "88" #### Configuration ``` -max_line_length = 88 +max-line-length = 88 ignore = E203 ``` From eabff673b37c5430d4cf72fa050a189a57be2deb Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 22 Jan 2023 18:51:09 +0530 Subject: [PATCH 405/700] Format hex code in unicode escape sequences in string literals (#2916) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/linegen.py | 4 ++ src/black/mode.py | 1 + src/black/strings.py | 44 ++++++++++++++++++- .../data/preview/format_unicode_escape_seq.py | 33 ++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/data/preview/format_unicode_escape_seq.py diff --git a/CHANGES.md b/CHANGES.md index 1450278341b..e2e4b341761 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Format hex code in unicode escape sequences in string literals (#2916) - Add parentheses around `if`-`else` expressions (#2278) - Improve the performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) diff --git a/src/black/linegen.py b/src/black/linegen.py index 2f50257a930..bfc28ca006c 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -59,6 +59,7 @@ get_string_prefix, normalize_string_prefix, normalize_string_quotes, + normalize_unicode_escape_sequences, ) from black.trans import ( CannotTransform, @@ -368,6 +369,9 @@ def visit_factor(self, node: Node) -> Iterator[Line]: yield from self.visit_default(node) def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: + if Preview.hex_codes_in_unicode_sequences in self.mode: + normalize_unicode_escape_sequences(leaf) + if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. diff --git a/src/black/mode.py b/src/black/mode.py index af0706e6a0b..4309d4fa635 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -153,6 +153,7 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" + hex_codes_in_unicode_sequences = auto() annotation_parens = auto() empty_lines_before_class_or_def_with_leading_comments = auto() handle_trailing_commas_in_head = auto() diff --git a/src/black/strings.py b/src/black/strings.py index 9d0e2eb8430..3e3bc12fe72 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -5,7 +5,9 @@ import re import sys from functools import lru_cache -from typing import List, Pattern +from typing import List, Match, Pattern + +from blib2to3.pytree import Leaf if sys.version_info < (3, 8): from typing_extensions import Final @@ -18,6 +20,15 @@ r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL ) FIRST_NON_WHITESPACE_RE: Final = re.compile(r"\s*\t+\s*(\S)") +UNICODE_ESCAPE_RE: Final = re.compile( + r"(?P\\+)(?P" + r"(u(?P[a-fA-F0-9]{4}))" # Character with 16-bit hex value xxxx + r"|(U(?P[a-fA-F0-9]{8}))" # Character with 32-bit hex value xxxxxxxx + r"|(x(?P[a-fA-F0-9]{2}))" # Character with hex value hh + r"|(N\{(?P[a-zA-Z0-9 \-]{2,})\})" # Character named name in the Unicode database + r")", + re.VERBOSE, +) def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: @@ -236,3 +247,34 @@ def normalize_string_quotes(s: str) -> str: return s # Prefer double quotes return f"{prefix}{new_quote}{new_body}{new_quote}" + + +def normalize_unicode_escape_sequences(leaf: Leaf) -> None: + """Replace hex codes in Unicode escape sequences with lowercase representation.""" + text = leaf.value + prefix = get_string_prefix(text) + if "r" in prefix.lower(): + return + + def replace(m: Match[str]) -> str: + groups = m.groupdict() + back_slashes = groups["backslashes"] + + if len(back_slashes) % 2 == 0: + return back_slashes + groups["body"] + + if groups["u"]: + # \u + return back_slashes + "u" + groups["u"].lower() + elif groups["U"]: + # \U + return back_slashes + "U" + groups["U"].lower() + elif groups["x"]: + # \x + return back_slashes + "x" + groups["x"].lower() + else: + assert groups["N"], f"Unexpected match: {m}" + # \N{} + return back_slashes + "N{" + groups["N"].upper() + "}" + + leaf.value = re.sub(UNICODE_ESCAPE_RE, replace, text) diff --git a/tests/data/preview/format_unicode_escape_seq.py b/tests/data/preview/format_unicode_escape_seq.py new file mode 100644 index 00000000000..3440696c303 --- /dev/null +++ b/tests/data/preview/format_unicode_escape_seq.py @@ -0,0 +1,33 @@ +x = "\x1F" +x = "\\x1B" +x = "\\\x1B" +x = "\U0001F60E" +x = "\u0001F60E" +x = r"\u0001F60E" +x = "don't format me" +x = "\xA3" +x = "\u2717" +x = "\uFaCe" +x = "\N{ox}\N{OX}" +x = "\N{lAtIn smaLL letteR x}" +x = "\N{CYRILLIC small LETTER BYELORUSSIAN-UKRAINIAN I}" +x = b"\x1Fdon't byte" +x = rb"\x1Fdon't format" + +# output + +x = "\x1f" +x = "\\x1B" +x = "\\\x1b" +x = "\U0001f60e" +x = "\u0001F60E" +x = r"\u0001F60E" +x = "don't format me" +x = "\xa3" +x = "\u2717" +x = "\uface" +x = "\N{OX}\N{OX}" +x = "\N{LATIN SMALL LETTER X}" +x = "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}" +x = b"\x1fdon't byte" +x = rb"\x1Fdon't format" From a36878eb2f375e2ac1e13052f663909f3835ec46 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sun, 22 Jan 2023 05:27:11 -0800 Subject: [PATCH 406/700] Fix an invalid quote escaping bug in f-string expressions (#3509) Fixes #3506 We can't simply escape the quotes in a naked f-string when merging string groups, because backslashes are invalid. The quotes in f-string expressions should be toggled (this is safe since quotes can't be reused). This fix also means implicitly concatenated f-strings with different quotes can now be merged or quote-normalized by changing the quotes used in expressions. e.g.: ```diff raise sa_exc.UnboundExecutionError( "Could not locate a bind configured on " - f'{", ".join(context)} or this Session.' + f"{', '.join(context)} or this Session." ) ``` --- CHANGES.md | 3 ++ src/black/trans.py | 30 +++++++++++++++++++ .../data/preview/long_strings__regression.py | 18 +++++++++++ 3 files changed, 51 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e2e4b341761..e311c492789 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,9 @@ - Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489) - Fix several crashes in preview style with walrus operators used in `with` statements or tuples (#3473) +- Fix an invalid quote escaping bug in f-string expressions where it produced invalid + code. Implicitly concatenated f-strings with different quotes can now be merged or + quote-normalized by changing the quotes used in expressions. (#3509) ### Configuration diff --git a/src/black/trans.py b/src/black/trans.py index ec07f5eab74..2360c13f06a 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -572,6 +572,12 @@ def make_naked(string: str, string_prefix: str) -> str: characters have been escaped. """ assert_is_leaf_string(string) + if "f" in string_prefix: + string = _toggle_fexpr_quotes(string, QUOTE) + # After quotes toggling, quotes in expressions won't be escaped + # because quotes can't be reused in f-strings. So we can simply + # let the escaping logic below run without knowing f-string + # expressions. RE_EVEN_BACKSLASHES = r"(?:(? bool: return any(iter_fexpr_spans(s)) +def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str: + """ + Toggles quotes used in f-string expressions that are `old_quote`. + + f-string expressions can't contain backslashes, so we need to toggle the + quotes if the f-string itself will end up using the same quote. We can + simply toggle without escaping because, quotes can't be reused in f-string + expressions. They will fail to parse. + + NOTE: If PEP 701 is accepted, above statement will no longer be true. + Though if quotes can be reused, we can simply reuse them without updates or + escaping, once Black figures out how to parse the new grammar. + """ + new_quote = "'" if old_quote == '"' else '"' + parts = [] + previous_index = 0 + for start, end in iter_fexpr_spans(fstring): + parts.append(fstring[previous_index:start]) + parts.append(fstring[start:end].replace(old_quote, new_quote)) + previous_index = end + parts.append(fstring[previous_index:]) + return "".join(parts) + + class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): """ StringTransformer that splits "atom" strings (i.e. strings which exist on diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index ef9007f4ce1..eead8c204a9 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -550,6 +550,16 @@ async def foo(self): ("item1", "item2", "item3"), } +# Regression test for https://github.com/psf/black/issues/3506. +s = ( + "With single quote: ' " + f" {my_dict['foo']}" + ' With double quote: " ' + f' {my_dict["bar"]}' +) + +s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:\'{my_dict["foo"]}\'' + # output @@ -1235,3 +1245,11 @@ async def foo(self): # And there is a comment before the value ("item1", "item2", "item3"), } + +# Regression test for https://github.com/psf/black/issues/3506. +s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}" + +s = ( + "Lorem Ipsum is simply dummy text of the printing and typesetting" + f" industry:'{my_dict['foo']}'" +) From d950f15987a49a5f3e37127ec718b4c12666b8cf Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Mon, 23 Jan 2023 21:38:30 -0800 Subject: [PATCH 407/700] Update document now that paren wrapping CMs on Python 3.9+ is implemented (#3520) --- docs/the_black_code_style/future_style.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 9ca260fc0ad..7a0b2d8f07a 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -31,8 +31,8 @@ with \ ... # backslashes and an ugly stranded colon ``` -Although when the target version is Python 3.9 or higher, _Black_ will, when we -implement this, use parentheses instead since they're allowed in Python 3.9 and higher. +Although when the target version is Python 3.9 or higher, _Black_ uses parentheses +instead in `--preview` mode (see below) since they're allowed in Python 3.9 and higher. An alternative to consider if the backslashes in the above formatting are undesirable is to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the From 196b1f349eb2baa9bbbc483226874cc01fb7567d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Tue, 24 Jan 2023 08:43:24 -0500 Subject: [PATCH 408/700] Fix `black --help` output for `--python-cell-magics` option to be reproducible (#3516) --- CHANGES.md | 2 ++ src/black/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e311c492789..2acb31d6ac4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,8 @@ +- Calling `black --help` multiple times will return the same help contents each time + (#3516) - Verbose logging now shows the values of `pyproject.toml` configuration variables (#3392) - Fix false symlink detection messages in verbose output due to using an incorrect diff --git a/src/black/__init__.py b/src/black/__init__.py index daf6f88f58e..f24487fd398 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -244,7 +244,7 @@ def validate_regex( multiple=True, help=( "When processing Jupyter Notebooks, add the given magic to the list" - f" of known python-magics ({', '.join(PYTHON_CELL_MAGICS)})." + f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})." " Useful for formatting cells with custom python magics." ), default=[], From 6407ebb870afe0062ee581abdea07c1ef5213d31 Mon Sep 17 00:00:00 2001 From: Evan Chen Date: Sat, 28 Jan 2023 16:12:11 -0800 Subject: [PATCH 409/700] Remove Python version in the_basics.md (#3528) --- docs/contributing/the_basics.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 9325a9e44ed..5fdcdd802bd 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -4,8 +4,8 @@ An overview on contributing to the _Black_ project. ## Technicalities -Development on the latest version of Python is preferred. As of this writing it's 3.9. -You can use any operating system. +Development on the latest version of Python is preferred. You can use any operating +system. Install development dependencies inside a virtual environment of your choice, for example: From f4ebc683208d095b252b87147d002e925c9c1171 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 30 Jan 2023 18:45:12 -0800 Subject: [PATCH 410/700] Upgrade isort (#3534) See PyCQA/isort#2077. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d5ac26b629..576f6405d6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: additional_dependencies: *version_check_dependencies - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort From 226cbf0226ee3bc26972357ba54c36409e9a84ae Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 30 Jan 2023 18:53:14 -0800 Subject: [PATCH 411/700] Fix unsafe cast in linegen.py w/ await yield handling (#3533) Fixes #3532. --- CHANGES.md | 1 + src/black/linegen.py | 25 ++++++++++++----------- tests/data/preview/remove_await_parens.py | 7 +++++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2acb31d6ac4..8c6a4f40166 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -42,6 +42,7 @@ - Fix an invalid quote escaping bug in f-string expressions where it produced invalid code. Implicitly concatenated f-strings with different quotes can now be merged or quote-normalized by changing the quotes used in expressions. (#3509) +- Fix crash on `await (yield)` when Black is compiled with mypyc (#3533) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index bfc28ca006c..9894a39c95f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1223,18 +1223,19 @@ def remove_await_parens(node: Node) -> None: # N.B. We've still removed any redundant nested brackets though :) opening_bracket = cast(Leaf, node.children[1].children[0]) closing_bracket = cast(Leaf, node.children[1].children[-1]) - bracket_contents = cast(Node, node.children[1].children[1]) - if bracket_contents.type != syms.power: - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - elif ( - bracket_contents.type == syms.power - and bracket_contents.children[0].type == token.AWAIT - ): - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - # If we are in a nested await then recurse down. - remove_await_parens(bracket_contents) + bracket_contents = node.children[1].children[1] + if isinstance(bracket_contents, Node): + if bracket_contents.type != syms.power: + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + elif ( + bracket_contents.type == syms.power + and bracket_contents.children[0].type == token.AWAIT + ): + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + # If we are in a nested await then recurse down. + remove_await_parens(bracket_contents) def _maybe_wrap_cms_in_parens( diff --git a/tests/data/preview/remove_await_parens.py b/tests/data/preview/remove_await_parens.py index 571210a2d80..8c7223d2f39 100644 --- a/tests/data/preview/remove_await_parens.py +++ b/tests/data/preview/remove_await_parens.py @@ -77,6 +77,9 @@ async def main(): async def main(): await (await (await (await (await (asyncio.sleep(1)))))) +async def main(): + await (yield) + # output import asyncio @@ -167,3 +170,7 @@ async def main(): async def main(): await (await (await (await (await asyncio.sleep(1))))) + + +async def main(): + await (yield) From c4bd2e31ceeac84d68592986fe70920f3d3d0443 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 31 Jan 2023 15:39:56 -0800 Subject: [PATCH 412/700] Draft for Black 2023 stable style (#3418) --- CHANGES.md | 29 ++++++ docs/the_black_code_style/current_style.md | 40 +++++++- docs/the_black_code_style/future_style.md | 90 ----------------- src/black/__init__.py | 9 +- src/black/comments.py | 58 +++++------ src/black/linegen.py | 99 +++++++------------ src/black/lines.py | 38 +++---- src/black/mode.py | 8 -- .../expression_skip_magic_trailing_comma.diff | 3 +- .../return_annotation_brackets_string.py | 12 +++ .../py_310/parenthesized_context_managers.py | 24 +++++ .../remove_newline_after_match.py | 0 tests/data/py_311/pep_654_style.py | 2 +- tests/data/py_38/pep_572_remove_parens.py | 4 +- .../remove_with_brackets.py | 0 tests/data/simple_cases/comments2.py | 1 + tests/data/simple_cases/comments3.py | 5 +- tests/data/simple_cases/comments5.py | 2 + .../{preview => simple_cases}/comments8.py | 0 .../{preview => simple_cases}/comments9.py | 0 .../docstring_preview.py | 0 tests/data/simple_cases/empty_lines.py | 1 - tests/data/simple_cases/fmtonoff.py | 1 + .../simple_cases/function_trailing_comma.py | 16 ++- .../one_element_subscript.py | 0 .../prefer_rhs_split_reformatted.py | 0 .../remove_await_parens.py | 0 .../remove_except_parens.py | 0 .../remove_for_brackets.py | 0 .../remove_newline_after_code_block_open.py | 0 .../return_annotation_brackets.py | 12 --- .../skip_magic_trailing_comma.py | 0 .../trailing_commas_in_leading_parts.py | 0 .../{preview => simple_cases}/whitespace.py | 0 tests/test_black.py | 3 +- tests/test_format.py | 24 +---- 36 files changed, 207 insertions(+), 274 deletions(-) create mode 100644 tests/data/preview/return_annotation_brackets_string.py rename tests/data/{preview_310 => py_310}/remove_newline_after_match.py (100%) rename tests/data/{preview_39 => py_39}/remove_with_brackets.py (100%) rename tests/data/{preview => simple_cases}/comments8.py (100%) rename tests/data/{preview => simple_cases}/comments9.py (100%) rename tests/data/{preview => simple_cases}/docstring_preview.py (100%) rename tests/data/{preview => simple_cases}/one_element_subscript.py (100%) rename tests/data/{preview => simple_cases}/prefer_rhs_split_reformatted.py (100%) rename tests/data/{preview => simple_cases}/remove_await_parens.py (100%) rename tests/data/{preview => simple_cases}/remove_except_parens.py (100%) rename tests/data/{preview => simple_cases}/remove_for_brackets.py (100%) rename tests/data/{preview => simple_cases}/remove_newline_after_code_block_open.py (100%) rename tests/data/{preview => simple_cases}/return_annotation_brackets.py (93%) rename tests/data/{preview => simple_cases}/skip_magic_trailing_comma.py (100%) rename tests/data/{preview => simple_cases}/trailing_commas_in_leading_parts.py (100%) rename tests/data/{preview => simple_cases}/whitespace.py (100%) diff --git a/CHANGES.md b/CHANGES.md index 8c6a4f40166..ecc8a41f505 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,35 @@ +- Introduce the 2023 stable style, which incorporates most aspects of last year's + preview style (#3418). Specific changes: + - Enforce empty lines before classes and functions with sticky leading comments + (#3302) (22.12.0) + - Reformat empty and whitespace-only files as either an empty file (if no newline is + present) or as a single newline character (if a newline is present) (#3348) + (22.12.0) + - Implicitly concatenated strings used as function args are now wrapped inside + parentheses (#3307) (22.12.0) + - Correctly handle trailing commas that are inside a line's leading non-nested parens + (#3370) (22.12.0) + - `--skip-string-normalization` / `-S` now prevents docstring prefixes from being + normalized as expected (#3168) (since 22.8.0) + - When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from + subscript expressions with more than 1 element (#3209) (22.8.0) + - Implicitly concatenated strings inside a list, set, or tuple are now wrapped inside + parentheses (#3162) (22.8.0) + - Fix a string merging/split issue when a comment is present in the middle of + implicitly concatenated strings on its own line (#3227) (22.8.0) + - Docstring quotes are no longer moved if it would violate the line length limit + (#3044, #3430) (22.6.0) + - Parentheses around return annotations are now managed (#2990) (22.6.0) + - Remove unnecessary parentheses around awaited objects (#2991) (22.6.0) + - Remove unnecessary parentheses in `with` statements (#2926) (22.6.0) + - Remove trailing newlines after code block open (#3035) (22.6.0) + - Code cell separators `#%%` are now standardised to `# %%` (#2919) (22.3.0) + - Remove unnecessary parentheses from `except` statements (#2939) (22.3.0) + - Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) (22.3.0) + - Avoid magic-trailing-comma in single-element subscripts (#2942) (22.3.0) - Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439) ### Preview style diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 56b92529d70..83f8785cc55 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -194,7 +194,45 @@ that in-function vertical whitespace should only be used sparingly. _Black_ will allow single empty lines inside functions, and single and double empty lines on module level left by the original editors, except when they're within parenthesized expressions. Since such expressions are always reformatted to fit minimal -space, this whitespace is lost. +space, this whitespace is lost. The other exception is that it will remove any empty +lines immediately following a statement that introduces a new indentation level. + +```python +# in: + +def foo(): + + print("All the newlines above me should be deleted!") + + +if condition: + + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +class Point: + + x: int + y: int + +# out: + +def foo(): + print("All the newlines above me should be deleted!") + + +if condition: + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +class Point: + x: int + y: int +``` It will also insert proper spacing before and after function definitions. It's one line before and after inner functions and two lines before and after module-level functions diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 7a0b2d8f07a..6d289d460a7 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -62,93 +62,3 @@ plain strings. User-made splits are respected when they do not exceed the line l limit. Line continuation backslashes are converted into parenthesized strings. Unnecessary parentheses are stripped. The stability and status of this feature is tracked in [this issue](https://github.com/psf/black/issues/2188). - -### Improved empty line management - -1. _Black_ will remove newlines in the beginning of new code blocks, i.e. when the - indentation level is increased. For example: - - ```python - def my_func(): - - print("The line above me will be deleted!") - ``` - - will be changed to: - - ```python - def my_func(): - print("The line above me will be deleted!") - ``` - - This new feature will be applied to **all code blocks**: `def`, `class`, `if`, - `for`, `while`, `with`, `case` and `match`. - -2. _Black_ will enforce empty lines before classes and functions with leading comments. - For example: - - ```python - some_var = 1 - # Leading sticky comment - def my_func(): - ... - ``` - - will be changed to: - - ```python - some_var = 1 - - - # Leading sticky comment - def my_func(): - ... - ``` - -### Improved parentheses management - -_Black_ will format parentheses around return annotations similarly to other sets of -parentheses. For example: - -```python -def foo() -> (int): - ... - -def foo() -> looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong: - ... -``` - -will be changed to: - -```python -def foo() -> int: - ... - - -def foo() -> ( - looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong -): - ... -``` - -And, extra parentheses in `await` expressions and `with` statements are removed. For -example: - -```python -with ((open("bla.txt")) as f, open("x")): - ... - -async def main(): - await (asyncio.sleep(1)) -``` - -will be changed to: - -```python -with open("bla.txt") as f, open("x"): - ... - - -async def main(): - await asyncio.sleep(1) -``` diff --git a/src/black/__init__.py b/src/black/__init__.py index f24487fd398..42bdfc1a5dd 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -925,9 +925,6 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. `mode` is passed to :func:`format_str`. """ - if not mode.preview and not src_contents.strip(): - raise NothingChanged - if mode.is_ipynb: dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode) else: @@ -1022,7 +1019,7 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon Operate cell-by-cell, only on code cells, only for Python notebooks. If the ``.ipynb`` originally had a trailing newline, it'll be preserved. """ - if mode.preview and not src_contents: + if not src_contents: raise NothingChanged trailing_newline = src_contents[-1] == "\n" @@ -1101,7 +1098,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} if supports_feature(versions, feature) } - normalize_fmt_off(src_node, preview=mode.preview) + normalize_fmt_off(src_node) lines = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { @@ -1122,7 +1119,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: dst_contents = [] for block in dst_blocks: dst_contents.extend(block.all_lines()) - if mode.preview and not dst_contents: + if not dst_contents: # Use decode_bytes to retrieve the correct source newline (CRLF or LF), # and check if normalized_content has more than one line normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8")) diff --git a/src/black/comments.py b/src/black/comments.py index e733dccd844..7cf15bf67b3 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -29,7 +29,7 @@ FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} -COMMENT_EXCEPTIONS = {True: " !:#'", False: " !:#'%"} +COMMENT_EXCEPTIONS = " !:#'" @dataclass @@ -50,7 +50,7 @@ class ProtoComment: consumed: int # how many characters of the original leaf's prefix did we consume -def generate_comments(leaf: LN, *, preview: bool) -> Iterator[Leaf]: +def generate_comments(leaf: LN) -> Iterator[Leaf]: """Clean the prefix of the `leaf` and generate comments from it, if any. Comments in lib2to3 are shoved into the whitespace prefix. This happens @@ -69,16 +69,12 @@ def generate_comments(leaf: LN, *, preview: bool) -> Iterator[Leaf]: Inline comments are emitted as regular token.COMMENT leaves. Standalone are emitted with a fake STANDALONE_COMMENT token identifier. """ - for pc in list_comments( - leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, preview=preview - ): + for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER): yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) @lru_cache(maxsize=4096) -def list_comments( - prefix: str, *, is_endmarker: bool, preview: bool -) -> List[ProtoComment]: +def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`.""" result: List[ProtoComment] = [] if not prefix or "#" not in prefix: @@ -104,7 +100,7 @@ def list_comments( comment_type = token.COMMENT # simple trailing comment else: comment_type = STANDALONE_COMMENT - comment = make_comment(line, preview=preview) + comment = make_comment(line) result.append( ProtoComment( type=comment_type, value=comment, newlines=nlines, consumed=consumed @@ -114,7 +110,7 @@ def list_comments( return result -def make_comment(content: str, *, preview: bool) -> str: +def make_comment(content: str) -> str: """Return a consistently formatted comment from the given `content` string. All comments (except for "##", "#!", "#:", '#'") should have a single @@ -135,26 +131,26 @@ def make_comment(content: str, *, preview: bool) -> str: and not content.lstrip().startswith("type:") ): content = " " + content[1:] # Replace NBSP by a simple space - if content and content[0] not in COMMENT_EXCEPTIONS[preview]: + if content and content[0] not in COMMENT_EXCEPTIONS: content = " " + content return "#" + content -def normalize_fmt_off(node: Node, *, preview: bool) -> None: +def normalize_fmt_off(node: Node) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node, preview=preview) + try_again = convert_one_fmt_off_pair(node) -def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: +def convert_one_fmt_off_pair(node: Node) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. """ for leaf in node.leaves(): previous_consumed = 0 - for comment in list_comments(leaf.prefix, is_endmarker=False, preview=preview): + for comment in list_comments(leaf.prefix, is_endmarker=False): if comment.value not in FMT_PASS: previous_consumed = comment.consumed continue @@ -169,7 +165,7 @@ def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: if comment.value in FMT_SKIP and prev.type in WHITESPACE: continue - ignored_nodes = list(generate_ignored_nodes(leaf, comment, preview=preview)) + ignored_nodes = list(generate_ignored_nodes(leaf, comment)) if not ignored_nodes: continue @@ -214,26 +210,24 @@ def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool: return False -def generate_ignored_nodes( - leaf: Leaf, comment: ProtoComment, *, preview: bool -) -> Iterator[LN]: +def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. Stops at the end of the block. """ if comment.value in FMT_SKIP: - yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, preview=preview) + yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment) return container: Optional[LN] = container_of(leaf) while container is not None and container.type != token.ENDMARKER: - if is_fmt_on(container, preview=preview): + if is_fmt_on(container): return # fix for fmt: on in children - if children_contains_fmt_on(container, preview=preview): + if children_contains_fmt_on(container): for index, child in enumerate(container.children): - if isinstance(child, Leaf) and is_fmt_on(child, preview=preview): + if isinstance(child, Leaf) and is_fmt_on(child): if child.type in CLOSING_BRACKETS: # This means `# fmt: on` is placed at a different bracket level # than `# fmt: off`. This is an invalid use, but as a courtesy, @@ -244,14 +238,12 @@ def generate_ignored_nodes( if ( child.type == token.INDENT and index < len(container.children) - 1 - and children_contains_fmt_on( - container.children[index + 1], preview=preview - ) + and children_contains_fmt_on(container.children[index + 1]) ): # This means `# fmt: on` is placed right after an indentation # level, and we shouldn't swallow the previous INDENT token. return - if children_contains_fmt_on(child, preview=preview): + if children_contains_fmt_on(child): return yield child else: @@ -264,14 +256,14 @@ def generate_ignored_nodes( def _generate_ignored_nodes_from_fmt_skip( - leaf: Leaf, comment: ProtoComment, *, preview: bool + leaf: Leaf, comment: ProtoComment ) -> Iterator[LN]: """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`.""" prev_sibling = leaf.prev_sibling parent = leaf.parent # Need to properly format the leaf prefix to compare it to comment.value, # which is also formatted - comments = list_comments(leaf.prefix, is_endmarker=False, preview=preview) + comments = list_comments(leaf.prefix, is_endmarker=False) if not comments or comment.value != comments[0].value: return if prev_sibling is not None: @@ -305,12 +297,12 @@ def _generate_ignored_nodes_from_fmt_skip( yield from iter(ignored_nodes) -def is_fmt_on(container: LN, preview: bool) -> bool: +def is_fmt_on(container: LN) -> bool: """Determine whether formatting is switched on within a container. Determined by whether the last `# fmt:` comment is `on` or `off`. """ fmt_on = False - for comment in list_comments(container.prefix, is_endmarker=False, preview=preview): + for comment in list_comments(container.prefix, is_endmarker=False): if comment.value in FMT_ON: fmt_on = True elif comment.value in FMT_OFF: @@ -318,11 +310,11 @@ def is_fmt_on(container: LN, preview: bool) -> bool: return fmt_on -def children_contains_fmt_on(container: LN, *, preview: bool) -> bool: +def children_contains_fmt_on(container: LN) -> bool: """Determine if children have formatting switched on.""" for child in container.children: leaf = first_leaf_of(child) - if leaf is not None and is_fmt_on(leaf, preview=preview): + if leaf is not None and is_fmt_on(leaf): return True return False diff --git a/src/black/linegen.py b/src/black/linegen.py index 9894a39c95f..7afb1733939 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -117,7 +117,7 @@ def visit_default(self, node: LN) -> Iterator[Line]: """Default `visit_*()` implementation. Recurses to children of `node`.""" if isinstance(node, Leaf): any_open_brackets = self.current_line.bracket_tracker.any_open_brackets() - for comment in generate_comments(node, preview=self.mode.preview): + for comment in generate_comments(node): if any_open_brackets: # any comment within brackets is subject to splitting self.current_line.append(comment) @@ -221,30 +221,27 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: def visit_funcdef(self, node: Node) -> Iterator[Line]: """Visit function definition.""" - if Preview.annotation_parens not in self.mode: - yield from self.visit_stmt(node, keywords={"def"}, parens=set()) - else: - yield from self.line() + yield from self.line() - # Remove redundant brackets around return type annotation. - is_return_annotation = False - for child in node.children: - if child.type == token.RARROW: - is_return_annotation = True - elif is_return_annotation: - if child.type == syms.atom and child.children[0].type == token.LPAR: - if maybe_make_parens_invisible_in_atom( - child, - parent=node, - remove_brackets_around_comma=False, - ): - wrap_in_parentheses(node, child, visible=False) - else: + # Remove redundant brackets around return type annotation. + is_return_annotation = False + for child in node.children: + if child.type == token.RARROW: + is_return_annotation = True + elif is_return_annotation: + if child.type == syms.atom and child.children[0].type == token.LPAR: + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_brackets_around_comma=False, + ): wrap_in_parentheses(node, child, visible=False) - is_return_annotation = False + else: + wrap_in_parentheses(node, child, visible=False) + is_return_annotation = False - for child in node.children: - yield from self.visit(child) + for child in node.children: + yield from self.visit(child) def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" @@ -332,8 +329,7 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - if Preview.remove_redundant_parens in self.mode: - remove_await_parens(node) + remove_await_parens(node) yield from self.visit_default(node) @@ -375,24 +371,17 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if is_docstring(leaf) and "\\\n" not in leaf.value: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. - if Preview.normalize_docstring_quotes_and_prefixes_properly in self.mode: - # There was a bug where --skip-string-normalization wouldn't stop us - # from normalizing docstring prefixes. To maintain stability, we can - # only address this buggy behaviour while the preview style is enabled. - if self.mode.string_normalization: - docstring = normalize_string_prefix(leaf.value) - # visit_default() does handle string normalization for us, but - # since this method acts differently depending on quote style (ex. - # see padding logic below), there's a possibility for unstable - # formatting as visit_default() is called *after*. To avoid a - # situation where this function formats a docstring differently on - # the second pass, normalize it early. - docstring = normalize_string_quotes(docstring) - else: - docstring = leaf.value - else: - # ... otherwise, we'll keep the buggy behaviour >.< + if self.mode.string_normalization: docstring = normalize_string_prefix(leaf.value) + # visit_default() does handle string normalization for us, but + # since this method acts differently depending on quote style (ex. + # see padding logic below), there's a possibility for unstable + # formatting as visit_default() is called *after*. To avoid a + # situation where this function formats a docstring differently on + # the second pass, normalize it early. + docstring = normalize_string_quotes(docstring) + else: + docstring = leaf.value prefix = get_string_prefix(docstring) docstring = docstring[len(prefix) :] # Remove the prefix quote_char = docstring[0] @@ -432,7 +421,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote = quote_char * quote_len # It's invalid to put closing single-character quotes on a new line. - if Preview.long_docstring_quotes_on_newline in self.mode and quote_len == 3: + if self.mode and quote_len == 3: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. @@ -473,14 +462,8 @@ def __post_init__(self) -> None: self.visit_try_stmt = partial( v, keywords={"try", "except", "else", "finally"}, parens=Ø ) - if self.mode.preview: - self.visit_except_clause = partial( - v, keywords={"except"}, parens={"except"} - ) - self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) - else: - self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) - self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) + self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"}) + self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) @@ -932,10 +915,7 @@ def bracket_split_build_line( break leaves_to_track: Set[LeafID] = set() - if ( - Preview.handle_trailing_commas_in_head in original.mode - and component is _BracketSplitComponent.head - ): + if component is _BracketSplitComponent.head: leaves_to_track = get_leaves_inside_matching_brackets(leaves) # Populate the line for leaf in leaves: @@ -1109,7 +1089,7 @@ def normalize_invisible_parens( Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. """ - for pc in list_comments(node.prefix, is_endmarker=False, preview=mode.preview): + for pc in list_comments(node.prefix, is_endmarker=False): if pc.value in FMT_OFF: # This `node` has a prefix with `# fmt: off`, don't mess with parens. return @@ -1139,8 +1119,7 @@ def normalize_invisible_parens( if check_lpar: if ( - mode.preview - and child.type == syms.atom + child.type == syms.atom and node.type == syms.for_stmt and isinstance(child.prev_sibling, Leaf) and child.prev_sibling.type == token.NAME @@ -1152,9 +1131,7 @@ def normalize_invisible_parens( remove_brackets_around_comma=True, ): wrap_in_parentheses(node, child, visible=False) - elif ( - mode.preview and isinstance(child, Node) and node.type == syms.with_stmt - ): + elif isinstance(child, Node) and node.type == syms.with_stmt: remove_with_parens(child, node) elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom( @@ -1180,7 +1157,7 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - comma_check = child.type == token.COMMA if mode.preview else False + comma_check = child.type == token.COMMA check_lpar = isinstance(child, Leaf) and ( child.value in parens_after or comma_check diff --git a/src/black/lines.py b/src/black/lines.py index 2aa675c3b31..ec6ef5d9522 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -14,7 +14,7 @@ ) from black.brackets import DOT_PRIORITY, BracketTracker -from black.mode import Mode, Preview +from black.mode import Mode from black.nodes import ( BRACKETS, CLOSING_BRACKETS, @@ -275,8 +275,7 @@ def has_magic_trailing_comma( - it's not a single-element subscript Additionally, if ensure_removable: - it's not from square bracket indexing - (specifically, single-element square bracket indexing with - Preview.skip_magic_trailing_comma_in_subscript) + (specifically, single-element square bracket indexing) """ if not ( closing.type in CLOSING_BRACKETS @@ -290,8 +289,7 @@ def has_magic_trailing_comma( if closing.type == token.RSQB: if ( - Preview.one_element_subscript in self.mode - and closing.parent + closing.parent and closing.parent.type == syms.trailer and closing.opening_bracket and is_one_sequence_between( @@ -309,18 +307,16 @@ def has_magic_trailing_comma( comma = self.leaves[-1] if comma.parent is None: return False - if Preview.skip_magic_trailing_comma_in_subscript in self.mode: - return ( - comma.parent.type != syms.subscriptlist - or closing.opening_bracket is None - or not is_one_sequence_between( - closing.opening_bracket, - closing, - self.leaves, - brackets=(token.LSQB, token.RSQB), - ) + return ( + comma.parent.type != syms.subscriptlist + or closing.opening_bracket is None + or not is_one_sequence_between( + closing.opening_bracket, + closing, + self.leaves, + brackets=(token.LSQB, token.RSQB), ) - return comma.parent.type == syms.listmaker + ) if self.is_import: return True @@ -592,11 +588,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: ): return before, 1 - if ( - Preview.remove_block_trailing_newline in current_line.mode - and self.previous_line - and self.previous_line.opens_block - ): + if self.previous_line and self.previous_line.opens_block: return 0, 0 return before, 0 @@ -629,9 +621,7 @@ def _maybe_empty_lines_for_class_or_def( ): slc = self.semantic_leading_comment if ( - Preview.empty_lines_before_class_or_def_with_leading_comments - in current_line.mode - and slc is not None + slc is not None and slc.previous_block is not None and not slc.previous_block.original_line.is_class and not slc.previous_block.original_line.opens_block diff --git a/src/black/mode.py b/src/black/mode.py index 4309d4fa635..1af16070073 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -154,15 +154,7 @@ class Preview(Enum): """Individual preview style features.""" hex_codes_in_unicode_sequences = auto() - annotation_parens = auto() - empty_lines_before_class_or_def_with_leading_comments = auto() - handle_trailing_commas_in_head = auto() - long_docstring_quotes_on_newline = auto() - normalize_docstring_quotes_and_prefixes_properly = auto() - one_element_subscript = auto() prefer_splitting_right_hand_side_of_assignments = auto() - remove_block_trailing_newline = auto() - remove_redundant_parens = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() diff --git a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff index eba3fd2da7d..d17467b15c7 100644 --- a/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff +++ b/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff @@ -144,8 +144,9 @@ -tuple[ - str, int, float, dict[str, int] -] +-tuple[str, int, float, dict[str, int],] ++tuple[str, int, float, dict[str, int]] +tuple[str, int, float, dict[str, int]] - tuple[str, int, float, dict[str, int],] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], ] diff --git a/tests/data/preview/return_annotation_brackets_string.py b/tests/data/preview/return_annotation_brackets_string.py new file mode 100644 index 00000000000..6978829fd5c --- /dev/null +++ b/tests/data/preview/return_annotation_brackets_string.py @@ -0,0 +1,12 @@ +# Long string example +def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": + pass + +# output + +# Long string example +def frobnicate() -> ( + "ThisIsTrulyUnreasonablyExtremelyLongClassName |" + " list[ThisIsTrulyUnreasonablyExtremelyLongClassName]" +): + pass diff --git a/tests/data/py_310/parenthesized_context_managers.py b/tests/data/py_310/parenthesized_context_managers.py index ccf1f94883e..1ef09a1bd34 100644 --- a/tests/data/py_310/parenthesized_context_managers.py +++ b/tests/data/py_310/parenthesized_context_managers.py @@ -19,3 +19,27 @@ CtxManager3() as example3, ): ... + +# output + +with CtxManager() as example: + ... + +with CtxManager1(), CtxManager2(): + ... + +with CtxManager1() as example, CtxManager2(): + ... + +with CtxManager1(), CtxManager2() as example: + ... + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/tests/data/preview_310/remove_newline_after_match.py b/tests/data/py_310/remove_newline_after_match.py similarity index 100% rename from tests/data/preview_310/remove_newline_after_match.py rename to tests/data/py_310/remove_newline_after_match.py diff --git a/tests/data/py_311/pep_654_style.py b/tests/data/py_311/pep_654_style.py index 568e5e3efa4..9fc7c0c84db 100644 --- a/tests/data/py_311/pep_654_style.py +++ b/tests/data/py_311/pep_654_style.py @@ -76,7 +76,7 @@ except: try: raise TypeError(int) - except* (Exception): + except* Exception: pass 1 / 0 except Exception as e: diff --git a/tests/data/py_38/pep_572_remove_parens.py b/tests/data/py_38/pep_572_remove_parens.py index 4e95fb07f3a..b952b2940c5 100644 --- a/tests/data/py_38/pep_572_remove_parens.py +++ b/tests/data/py_38/pep_572_remove_parens.py @@ -62,7 +62,6 @@ async def await_the_walrus(): with (x := await a, y := await b): pass - # Ideally we should remove one set of parentheses with ((x := await a, y := await b)): pass @@ -137,8 +136,7 @@ async def await_the_walrus(): with (x := await a, y := await b): pass - # Ideally we should remove one set of parentheses - with ((x := await a, y := await b)): + with (x := await a, y := await b): pass with (x := await a), (y := await b): diff --git a/tests/data/preview_39/remove_with_brackets.py b/tests/data/py_39/remove_with_brackets.py similarity index 100% rename from tests/data/preview_39/remove_with_brackets.py rename to tests/data/py_39/remove_with_brackets.py diff --git a/tests/data/simple_cases/comments2.py b/tests/data/simple_cases/comments2.py index 4eea013151a..37e185abf4f 100644 --- a/tests/data/simple_cases/comments2.py +++ b/tests/data/simple_cases/comments2.py @@ -226,6 +226,7 @@ def _init_host(self, parsed) -> None: add_compiler(compilers[(7.0, 32)]) # add_compiler(compilers[(7.1, 64)]) + # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: diff --git a/tests/data/simple_cases/comments3.py b/tests/data/simple_cases/comments3.py index fbbef6dcc6b..f964bee6651 100644 --- a/tests/data/simple_cases/comments3.py +++ b/tests/data/simple_cases/comments3.py @@ -1,6 +1,7 @@ # The percent-percent comments are Spyder IDE cells. -#%% + +# %% def func(): x = """ a really long string @@ -44,4 +45,4 @@ def func(): ) -#%% \ No newline at end of file +# %% \ No newline at end of file diff --git a/tests/data/simple_cases/comments5.py b/tests/data/simple_cases/comments5.py index c8c38813d55..bda40619f62 100644 --- a/tests/data/simple_cases/comments5.py +++ b/tests/data/simple_cases/comments5.py @@ -62,6 +62,8 @@ def decorated1(): # Preview.empty_lines_before_class_or_def_with_leading_comments. # In the current style, the user will have to split those lines by hand. some_instruction + + # This comment should be split from `some_instruction` by two lines but isn't. def g(): ... diff --git a/tests/data/preview/comments8.py b/tests/data/simple_cases/comments8.py similarity index 100% rename from tests/data/preview/comments8.py rename to tests/data/simple_cases/comments8.py diff --git a/tests/data/preview/comments9.py b/tests/data/simple_cases/comments9.py similarity index 100% rename from tests/data/preview/comments9.py rename to tests/data/simple_cases/comments9.py diff --git a/tests/data/preview/docstring_preview.py b/tests/data/simple_cases/docstring_preview.py similarity index 100% rename from tests/data/preview/docstring_preview.py rename to tests/data/simple_cases/docstring_preview.py diff --git a/tests/data/simple_cases/empty_lines.py b/tests/data/simple_cases/empty_lines.py index 4c03e432383..4fd47b93dca 100644 --- a/tests/data/simple_cases/empty_lines.py +++ b/tests/data/simple_cases/empty_lines.py @@ -119,7 +119,6 @@ def f(): if not prev: prevp = preceding_leaf(p) if not prevp or prevp.type in OPENING_BRACKETS: - return NO if prevp.type == token.EQUAL: diff --git a/tests/data/simple_cases/fmtonoff.py b/tests/data/simple_cases/fmtonoff.py index 5a50eb12ed3..e40ea2c8d21 100644 --- a/tests/data/simple_cases/fmtonoff.py +++ b/tests/data/simple_cases/fmtonoff.py @@ -205,6 +205,7 @@ def single_literal_yapf_disable(): # Comment 2 + # fmt: off def func_no_args(): a; b; c diff --git a/tests/data/simple_cases/function_trailing_comma.py b/tests/data/simple_cases/function_trailing_comma.py index abe9617c0e9..92f46e27516 100644 --- a/tests/data/simple_cases/function_trailing_comma.py +++ b/tests/data/simple_cases/function_trailing_comma.py @@ -116,9 +116,9 @@ def f( pass -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -]: +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( + Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] +): json = { "k": { "k2": { @@ -140,9 +140,7 @@ def some_function_with_a_really_long_name() -> ( def some_method_with_a_really_long_name( very_long_parameter_so_yeah: str, another_long_parameter: int -) -> ( - another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not -): +) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: pass @@ -155,10 +153,8 @@ def func() -> ( def func() -> ( - ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too ) ): pass diff --git a/tests/data/preview/one_element_subscript.py b/tests/data/simple_cases/one_element_subscript.py similarity index 100% rename from tests/data/preview/one_element_subscript.py rename to tests/data/simple_cases/one_element_subscript.py diff --git a/tests/data/preview/prefer_rhs_split_reformatted.py b/tests/data/simple_cases/prefer_rhs_split_reformatted.py similarity index 100% rename from tests/data/preview/prefer_rhs_split_reformatted.py rename to tests/data/simple_cases/prefer_rhs_split_reformatted.py diff --git a/tests/data/preview/remove_await_parens.py b/tests/data/simple_cases/remove_await_parens.py similarity index 100% rename from tests/data/preview/remove_await_parens.py rename to tests/data/simple_cases/remove_await_parens.py diff --git a/tests/data/preview/remove_except_parens.py b/tests/data/simple_cases/remove_except_parens.py similarity index 100% rename from tests/data/preview/remove_except_parens.py rename to tests/data/simple_cases/remove_except_parens.py diff --git a/tests/data/preview/remove_for_brackets.py b/tests/data/simple_cases/remove_for_brackets.py similarity index 100% rename from tests/data/preview/remove_for_brackets.py rename to tests/data/simple_cases/remove_for_brackets.py diff --git a/tests/data/preview/remove_newline_after_code_block_open.py b/tests/data/simple_cases/remove_newline_after_code_block_open.py similarity index 100% rename from tests/data/preview/remove_newline_after_code_block_open.py rename to tests/data/simple_cases/remove_newline_after_code_block_open.py diff --git a/tests/data/preview/return_annotation_brackets.py b/tests/data/simple_cases/return_annotation_brackets.py similarity index 93% rename from tests/data/preview/return_annotation_brackets.py rename to tests/data/simple_cases/return_annotation_brackets.py index 27760bd51d7..265c30220d8 100644 --- a/tests/data/preview/return_annotation_brackets.py +++ b/tests/data/simple_cases/return_annotation_brackets.py @@ -87,10 +87,6 @@ def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo def foo() -> tuple[int, int, int,]: return 2 -# Long string example -def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": - pass - # output # Control def double(a: int) -> int: @@ -212,11 +208,3 @@ def foo() -> ( ] ): return 2 - - -# Long string example -def frobnicate() -> ( - "ThisIsTrulyUnreasonablyExtremelyLongClassName |" - " list[ThisIsTrulyUnreasonablyExtremelyLongClassName]" -): - pass diff --git a/tests/data/preview/skip_magic_trailing_comma.py b/tests/data/simple_cases/skip_magic_trailing_comma.py similarity index 100% rename from tests/data/preview/skip_magic_trailing_comma.py rename to tests/data/simple_cases/skip_magic_trailing_comma.py diff --git a/tests/data/preview/trailing_commas_in_leading_parts.py b/tests/data/simple_cases/trailing_commas_in_leading_parts.py similarity index 100% rename from tests/data/preview/trailing_commas_in_leading_parts.py rename to tests/data/simple_cases/trailing_commas_in_leading_parts.py diff --git a/tests/data/preview/whitespace.py b/tests/data/simple_cases/whitespace.py similarity index 100% rename from tests/data/preview/whitespace.py rename to tests/data/simple_cases/whitespace.py diff --git a/tests/test_black.py b/tests/test_black.py index 44d617244f1..d0e78b7dd92 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -419,7 +419,8 @@ def test_skip_magic_trailing_comma(self) -> None: msg = ( "Expected diff isn't equal to the actual. If you made changes to" " expression.py and this is an anticipated difference, overwrite" - f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}" + " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff" + f" with {dump}" ) self.assertEqual(expected, actual, msg) diff --git a/tests/test_format.py b/tests/test_format.py index adcbc02468d..ab849aac9a3 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -32,31 +32,15 @@ def check_file( @pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") @pytest.mark.parametrize("filename", all_data_cases("simple_cases")) def test_simple_format(filename: str) -> None: - check_file("simple_cases", filename, DEFAULT_MODE) - - -@pytest.mark.parametrize("filename", all_data_cases("preview")) -def test_preview_format(filename: str) -> None: magic_trailing_comma = filename != "skip_magic_trailing_comma" check_file( - "preview", - filename, - black.Mode(preview=True, magic_trailing_comma=magic_trailing_comma), + "simple_cases", filename, black.Mode(magic_trailing_comma=magic_trailing_comma) ) -@pytest.mark.parametrize("filename", all_data_cases("preview_39")) -def test_preview_minimum_python_39_format(filename: str) -> None: - source, expected = read_data("preview_39", filename) - mode = black.Mode(preview=True) - assert_format(source, expected, mode, minimum_version=(3, 9)) - - -@pytest.mark.parametrize("filename", all_data_cases("preview_310")) -def test_preview_minimum_python_310_format(filename: str) -> None: - source, expected = read_data("preview_310", filename) - mode = black.Mode(preview=True) - assert_format(source, expected, mode, minimum_version=(3, 10)) +@pytest.mark.parametrize("filename", all_data_cases("preview")) +def test_preview_format(filename: str) -> None: + check_file("preview", filename, black.Mode(preview=True)) def test_preview_context_managers_targeting_py38() -> None: From 69ca0a4c7a365c5f5eea519a90980bab72cab764 Mon Sep 17 00:00:00 2001 From: Stijn de Gooijer Date: Wed, 1 Feb 2023 03:00:17 +0100 Subject: [PATCH 413/700] Infer target version based on project metadata (#3219) Co-authored-by: Richard Si --- .pre-commit-config.yaml | 1 + CHANGES.md | 5 + pyproject.toml | 1 + src/black/__init__.py | 5 +- src/black/files.py | 100 +++++++++++++++++- src/black/parsing.py | 6 +- .../data/project_metadata/both_pyproject.toml | 8 ++ .../project_metadata/neither_pyproject.toml | 6 ++ .../only_black_pyproject.toml | 7 ++ .../only_metadata_pyproject.toml | 7 ++ tests/test_black.py | 66 ++++++++++++ 11 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 tests/data/project_metadata/both_pyproject.toml create mode 100644 tests/data/project_metadata/neither_pyproject.toml create mode 100644 tests/data/project_metadata/only_black_pyproject.toml create mode 100644 tests/data/project_metadata/only_metadata_pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 576f6405d6c..a69fb645238 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,7 @@ repos: - tomli >= 0.2.6, < 2.0.0 - types-typed-ast >= 1.4.1 - click >= 8.1.0 + - packaging >= 22.0 - platformdirs >= 2.1.0 - pytest - hypothesis diff --git a/CHANGES.md b/CHANGES.md index ecc8a41f505..471567509d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -77,6 +77,9 @@ +- Black now tries to infer its `--target-version` from the project metadata specified in + `pyproject.toml` (#3219) + ### Packaging @@ -86,6 +89,8 @@ - Drop specific support for the `tomli` requirement on 3.11 alpha releases, working around a bug that would cause the requirement not to be installed on any non-final Python releases (#3448) +- Black now depends on `packaging` version `22.0` or later. This is required for new + functionality that needs to parse part of the project metadata (#3219) ### Parser diff --git a/pyproject.toml b/pyproject.toml index ab38908ba15..435626ac8f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ classifiers = [ dependencies = [ "click>=8.0.0", "mypy_extensions>=0.4.3", + "packaging>=22.0", "pathspec>=0.9.0", "platformdirs>=2", "tomli>=1.1.0; python_version < '3.11'", diff --git a/src/black/__init__.py b/src/black/__init__.py index 42bdfc1a5dd..4ebf28821c3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -219,8 +219,9 @@ def validate_regex( callback=target_version_option_callback, multiple=True, help=( - "Python versions that should be supported by Black's output. [default: per-file" - " auto-detection]" + "Python versions that should be supported by Black's output. By default, Black" + " will try to infer this from the project metadata in pyproject.toml. If this" + " does not yield conclusive results, Black will use per-file auto-detection." ), ) @click.option( diff --git a/src/black/files.py b/src/black/files.py index ea517f4ece9..8c0131126b7 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -18,6 +18,8 @@ ) from mypy_extensions import mypyc_attr +from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet +from packaging.version import InvalidVersion, Version from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError @@ -32,6 +34,7 @@ import tomli as tomllib from black.handle_ipynb_magics import jupyter_dependencies_are_installed +from black.mode import TargetVersion from black.output import err from black.report import Report @@ -108,14 +111,103 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: @mypyc_attr(patchable=True) def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: - """Parse a pyproject toml file, pulling out relevant parts for Black + """Parse a pyproject toml file, pulling out relevant parts for Black. - If parsing fails, will raise a tomllib.TOMLDecodeError + If parsing fails, will raise a tomllib.TOMLDecodeError. """ with open(path_config, "rb") as f: pyproject_toml = tomllib.load(f) - config = pyproject_toml.get("tool", {}).get("black", {}) - return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} + config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("black", {}) + config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} + + if "target_version" not in config: + inferred_target_version = infer_target_version(pyproject_toml) + if inferred_target_version is not None: + config["target_version"] = [v.name.lower() for v in inferred_target_version] + + return config + + +def infer_target_version( + pyproject_toml: Dict[str, Any] +) -> Optional[List[TargetVersion]]: + """Infer Black's target version from the project metadata in pyproject.toml. + + Supports the PyPA standard format (PEP 621): + https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#requires-python + + If the target version cannot be inferred, returns None. + """ + project_metadata = pyproject_toml.get("project", {}) + requires_python = project_metadata.get("requires-python", None) + if requires_python is not None: + try: + return parse_req_python_version(requires_python) + except InvalidVersion: + pass + try: + return parse_req_python_specifier(requires_python) + except (InvalidSpecifier, InvalidVersion): + pass + + return None + + +def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersion]]: + """Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion. + + If parsing fails, will raise a packaging.version.InvalidVersion error. + If the parsed version cannot be mapped to a valid TargetVersion, returns None. + """ + version = Version(requires_python) + if version.release[0] != 3: + return None + try: + return [TargetVersion(version.release[1])] + except (IndexError, ValueError): + return None + + +def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVersion]]: + """Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion. + + If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error. + If the parsed specifier cannot be mapped to a valid TargetVersion, returns None. + """ + specifier_set = strip_specifier_set(SpecifierSet(requires_python)) + if not specifier_set: + return None + + target_version_map = {f"3.{v.value}": v for v in TargetVersion} + compatible_versions: List[str] = list(specifier_set.filter(target_version_map)) + if compatible_versions: + return [target_version_map[v] for v in compatible_versions] + return None + + +def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: + """Strip minor versions for some specifiers in the specifier set. + + For background on version specifiers, see PEP 440: + https://peps.python.org/pep-0440/#version-specifiers + """ + specifiers = [] + for s in specifier_set: + if "*" in str(s): + specifiers.append(s) + elif s.operator in ["~=", "==", ">=", "==="]: + version = Version(s.version) + stripped = Specifier(f"{s.operator}{version.major}.{version.minor}") + specifiers.append(stripped) + elif s.operator == ">": + version = Version(s.version) + if len(version.release) > 2: + s = Specifier(f">={version.major}.{version.minor}") + specifiers.append(s) + else: + specifiers.append(s) + + return SpecifierSet(",".join(str(s) for s in specifiers)) @lru_cache() diff --git a/src/black/parsing.py b/src/black/parsing.py index c37c12b868d..ba474c5e047 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -11,7 +11,7 @@ else: from typing import Final -from black.mode import Feature, TargetVersion, supports_feature +from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature from black.nodes import syms from blib2to3 import pygram from blib2to3.pgen2 import driver @@ -52,7 +52,7 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: if not target_versions: # No target_version specified, so try all grammars. return [ - # Python 3.7+ + # Python 3.7-3.9 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, @@ -72,7 +72,7 @@ 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): + if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions): # Python 3.10+ grammars.append(pygram.python_grammar_soft_keywords) diff --git a/tests/data/project_metadata/both_pyproject.toml b/tests/data/project_metadata/both_pyproject.toml new file mode 100644 index 00000000000..cf8f148f856 --- /dev/null +++ b/tests/data/project_metadata/both_pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "test" +version = "1.0.0" +requires-python = ">=3.7,<3.11" + +[tool.black] +line-length = 79 +target-version = ["py310"] diff --git a/tests/data/project_metadata/neither_pyproject.toml b/tests/data/project_metadata/neither_pyproject.toml new file mode 100644 index 00000000000..67623d2279b --- /dev/null +++ b/tests/data/project_metadata/neither_pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "test" +version = "1.0.0" + +[tool.black] +line-length = 79 diff --git a/tests/data/project_metadata/only_black_pyproject.toml b/tests/data/project_metadata/only_black_pyproject.toml new file mode 100644 index 00000000000..94058bb3b1e --- /dev/null +++ b/tests/data/project_metadata/only_black_pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test" +version = "1.0.0" + +[tool.black] +line-length = 79 +target-version = ["py310"] diff --git a/tests/data/project_metadata/only_metadata_pyproject.toml b/tests/data/project_metadata/only_metadata_pyproject.toml new file mode 100644 index 00000000000..1c8cdbb31ad --- /dev/null +++ b/tests/data/project_metadata/only_metadata_pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test" +version = "1.0.0" +requires-python = ">=3.7,<3.11" + +[tool.black] +line-length = 79 diff --git a/tests/test_black.py b/tests/test_black.py index d0e78b7dd92..e5e17777715 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1560,6 +1560,72 @@ def test_parse_pyproject_toml(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + def test_parse_pyproject_toml_project_metadata(self) -> None: + for test_toml, expected in [ + ("only_black_pyproject.toml", ["py310"]), + ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]), + ("neither_pyproject.toml", None), + ("both_pyproject.toml", ["py310"]), + ]: + test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml + config = black.parse_pyproject_toml(str(test_toml_file)) + self.assertEqual(config.get("target_version"), expected) + + def test_infer_target_version(self) -> None: + for version, expected in [ + ("3.6", [TargetVersion.PY36]), + ("3.11.0rc1", [TargetVersion.PY311]), + (">=3.10", [TargetVersion.PY310, TargetVersion.PY311]), + (">=3.10.6", [TargetVersion.PY310, TargetVersion.PY311]), + ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]), + (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]), + (">3.7,!=3.8,!=3.9", [TargetVersion.PY310, TargetVersion.PY311]), + ( + "> 3.9.4, != 3.10.3", + [TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311], + ), + ( + "!=3.3,!=3.4", + [ + TargetVersion.PY35, + TargetVersion.PY36, + TargetVersion.PY37, + TargetVersion.PY38, + TargetVersion.PY39, + TargetVersion.PY310, + TargetVersion.PY311, + ], + ), + ( + "==3.*", + [ + TargetVersion.PY33, + TargetVersion.PY34, + TargetVersion.PY35, + TargetVersion.PY36, + TargetVersion.PY37, + TargetVersion.PY38, + TargetVersion.PY39, + TargetVersion.PY310, + TargetVersion.PY311, + ], + ), + ("==3.8.*", [TargetVersion.PY38]), + (None, None), + ("", None), + ("invalid", None), + ("==invalid", None), + (">3.9,!=invalid", None), + ("3", None), + ("3.2", None), + ("2.7.18", None), + ("==2.7", None), + (">3.10,<3.11", None), + ]: + test_toml = {"project": {"requires-python": version}} + result = black.files.infer_target_version(test_toml) + self.assertEqual(result, expected) + def test_read_pyproject_toml(self) -> None: test_toml_file = THIS_DIR / "test.toml" fake_ctx = FakeContext() From b0d1fba7ac3be53c71fb0d3211d911e629f8aecb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 31 Jan 2023 18:47:11 -0800 Subject: [PATCH 414/700] Prepare release 23.1.0 (#3536) Co-authored-by: Richard Si --- CHANGES.md | 78 +++++++++++++++++---- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 2 +- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 471567509d3..2071eb3f800 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,64 @@ +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + +## 23.1.0 + +### Highlights + +This is the first release of 2023, and following our +[stability policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy), +it comes with a number of improvements to our stable style, including improvements to +empty line handling, removal of redundant parentheses in several contexts, and output +that highlights implicitly concatenated strings better. + +There are also many changes to the preview style; try out `black --preview` and give us +feedback to help us set the stable style for next year. + +In addition to style changes, Black now automatically infers the supported Python +versions from your `pyproject.toml` file, removing the need to set Black's target +versions separately. + +### Stable style + + + - Introduce the 2023 stable style, which incorporates most aspects of last year's preview style (#3418). Specific changes: - Enforce empty lines before classes and functions with sticky leading comments @@ -45,9 +103,9 @@ -- Format hex code in unicode escape sequences in string literals (#2916) +- Format hex codes in unicode escape sequences in string literals (#2916) - Add parentheses around `if`-`else` expressions (#2278) -- Improve the performance on large expressions that contain many strings (#3467) +- Improve performance on large expressions that contain many strings (#3467) - Fix a crash in preview style with assert + parenthesized string (#3415) - Fix crashes in preview style with walrus operators used in function return annotations and except clauses (#3423) @@ -86,20 +144,14 @@ - Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7 (#3380) + - This also fixes some crashes while using compiled Black with a debug build of + CPython - Drop specific support for the `tomli` requirement on 3.11 alpha releases, working around a bug that would cause the requirement not to be installed on any non-final Python releases (#3448) - Black now depends on `packaging` version `22.0` or later. This is required for new functionality that needs to parse part of the project metadata (#3219) -### Parser - - - -### Performance - - - ### Output @@ -111,15 +163,11 @@ - Fix false symlink detection messages in verbose output due to using an incorrect relative path to the project root (#3385) -### _Blackd_ - - - ### Integrations -- Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446) +- Move 3.11 CI to normal flow now that all dependencies support 3.11 (#3446) - Docker: Add new `latest_prerelease` tag automation to follow latest black alpha release on docker images (#3465) diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 712b9a688d1..d462e2cc18a 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: 22.12.0 + rev: 23.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 9dc5277c61e..2b41c187766 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -178,7 +178,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, version 22.12.0 +black, version 23.1.0 ``` An option to require a specific version to be running is also provided. From dd0e912a6e7ebe432299e317e2e79b592fc2adc3 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 3 Feb 2023 22:00:09 -0800 Subject: [PATCH 415/700] Fix import of blib2to3.pgen2.driver (#3546) --- src/blib2to3/pgen2/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index d6deaac6964..c462f63ad2c 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -32,7 +32,7 @@ from blib2to3.pytree import convert, NL, Context, RawNode, Leaf, Node if TYPE_CHECKING: - from blib2to3.driver import TokenProxy + from blib2to3.pgen2.driver import TokenProxy Results = Dict[Text, NL] From ea5293b0360b6e2e56c004d1e27cbe33c87dcb94 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sat, 4 Feb 2023 19:30:47 -0800 Subject: [PATCH 416/700] Document the future style changes introduced in #3489 and #3440 (#3541) --- docs/the_black_code_style/future_style.md | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 6d289d460a7..b2cdca2590d 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -62,3 +62,52 @@ plain strings. User-made splits are respected when they do not exceed the line l limit. Line continuation backslashes are converted into parenthesized strings. Unnecessary parentheses are stripped. The stability and status of this feature is tracked in [this issue](https://github.com/psf/black/issues/2188). + +### Improved line breaks + +For assignment expressions, _Black_ now prefers to split and wrap the right side of the +assignment instead of left side. For example: + +```python +some_dict[ + "with_a_long_key" +] = some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument +) +``` + +will be changed to: + +```python +some_dict["with_a_long_key"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) +) +``` + +### Improved parentheses management + +For dict literals with long values, they are now wrapped in parentheses. Unnecessary +parentheses are now removed. For example: + +```python +my_dict = { + my_dict = { + "a key in my dict": a_very_long_variable + * and_a_very_long_function_call() + / 100000.0, + "another key": (short_value), +} +``` + +will be changed to: + +```python +my_dict = { + "a key in my dict": ( + a_very_long_variable * and_a_very_long_function_call() / 100000.0 + ), + "another key": short_value, +} +``` From ff53fc1b97b98075f92fcc954ed81b9252e7c9c1 Mon Sep 17 00:00:00 2001 From: mainj12 <118842653+mainj12@users.noreply.github.com> Date: Sun, 5 Feb 2023 03:35:43 +0000 Subject: [PATCH 417/700] Actually add trailing commas to collection literals even if there are terminating comments (#3393) Co-authored-by: Jelle Zijlstra Co-authored-by: Richard Si --- CHANGES.md | 3 + src/black/linegen.py | 62 ++++++++++++++----- src/black/mode.py | 1 + src/black/trans.py | 12 ++-- tests/data/preview/trailing_comma.py | 55 ++++++++++++++++ .../targeting_py39.py | 2 +- 6 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 tests/data/preview/trailing_comma.py diff --git a/CHANGES.md b/CHANGES.md index 2071eb3f800..a8b556cb7e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Add trailing commas to collection literals even if there's a comment after the last + entry (#3393) + ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 7afb1733939..c582b2d6dff 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -520,7 +520,7 @@ def transform_line( else: def _rhs( - self: object, line: Line, features: Collection[Feature] + self: object, line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: """Wraps calls to `right_hand_split`. @@ -604,7 +604,9 @@ class _BracketSplitComponent(Enum): tail = auto() -def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator[Line]: +def left_hand_split( + line: Line, _features: Collection[Feature], mode: Mode +) -> Iterator[Line]: """Split line into many lines, starting with the first matching bracket pair. Note: this usually looks weird, only use this for function definitions. @@ -940,16 +942,39 @@ def dont_increase_indentation(split_func: Transformer) -> Transformer: """ @wraps(split_func) - def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: - for split_line in split_func(line, features): + def split_wrapper( + line: Line, features: Collection[Feature], mode: Mode + ) -> Iterator[Line]: + for split_line in split_func(line, features, mode): normalize_prefix(split_line.leaves[0], inside_brackets=True) yield split_line return split_wrapper +def _get_last_non_comment_leaf(line: Line) -> Optional[int]: + for leaf_idx in range(len(line.leaves) - 1, 0, -1): + if line.leaves[leaf_idx].type != STANDALONE_COMMENT: + return leaf_idx + return None + + +def _safe_add_trailing_comma(safe: bool, delimiter_priority: int, line: Line) -> Line: + if ( + safe + and delimiter_priority == COMMA_PRIORITY + and line.leaves[-1].type != token.COMMA + and line.leaves[-1].type != STANDALONE_COMMENT + ): + new_comma = Leaf(token.COMMA, ",") + line.append(new_comma) + return line + + @dont_increase_indentation -def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: +def delimiter_split( + line: Line, features: Collection[Feature], mode: Mode +) -> Iterator[Line]: """Split according to delimiters of the highest priority. If the appropriate Features are given, the split will add trailing commas @@ -989,7 +1014,8 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: ) current_line.append(leaf) - for leaf in line.leaves: + last_non_comment_leaf = _get_last_non_comment_leaf(line) + for leaf_idx, leaf in enumerate(line.leaves): yield from append_to_line(leaf) for comment_after in line.comments_after(leaf): @@ -1006,6 +1032,15 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features ) + if ( + Preview.add_trailing_comma_consistently in mode + and last_leaf.type == STANDALONE_COMMENT + and leaf_idx == last_non_comment_leaf + ): + current_line = _safe_add_trailing_comma( + trailing_comma_safe, delimiter_priority, current_line + ) + leaf_priority = bt.delimiters.get(id(leaf)) if leaf_priority == delimiter_priority: yield current_line @@ -1014,20 +1049,15 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets ) if current_line: - if ( - trailing_comma_safe - and delimiter_priority == COMMA_PRIORITY - and current_line.leaves[-1].type != token.COMMA - and current_line.leaves[-1].type != STANDALONE_COMMENT - ): - new_comma = Leaf(token.COMMA, ",") - current_line.append(new_comma) + current_line = _safe_add_trailing_comma( + trailing_comma_safe, delimiter_priority, current_line + ) yield current_line @dont_increase_indentation def standalone_comment_split( - line: Line, features: Collection[Feature] = () + line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: """Split standalone comments from the rest of the line.""" if not line.contains_standalone_comments(0): @@ -1480,7 +1510,7 @@ def run_transformer( if not line_str: line_str = line_to_string(line) result: List[Line] = [] - for transformed_line in transform(line, features): + for transformed_line in transform(line, features, mode): if str(transformed_line).strip("\n") == line_str: raise CannotTransform("Line transformer returned an unchanged result") diff --git a/src/black/mode.py b/src/black/mode.py index 1af16070073..c9a4c2b080c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -153,6 +153,7 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" + add_trailing_comma_consistently = auto() hex_codes_in_unicode_sequences = auto() prefer_splitting_right_hand_side_of_assignments = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens diff --git a/src/black/trans.py b/src/black/trans.py index 2360c13f06a..a6a416e71bc 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -32,7 +32,7 @@ from black.comments import contains_pragma_comment from black.lines import Line, append_leaves -from black.mode import Feature +from black.mode import Feature, Mode from black.nodes import ( CLOSING_BRACKETS, OPENING_BRACKETS, @@ -63,7 +63,7 @@ class CannotTransform(Exception): # types T = TypeVar("T") LN = Union[Leaf, Node] -Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]] +Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]] Index = int NodeType = int ParserState = int @@ -81,7 +81,9 @@ def TErr(err_msg: str) -> Err[CannotTransform]: return Err(cant_transform) -def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]: +def hug_power_op( + line: Line, features: Collection[Feature], mode: Mode +) -> Iterator[Line]: """A transformer which normalizes spacing around power operators.""" # Performance optimization to avoid unnecessary Leaf clones and other ops. @@ -228,7 +230,9 @@ def do_transform( yield an CannotTransform after that point.) """ - def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]: + def __call__( + self, line: Line, _features: Collection[Feature], _mode: Mode + ) -> Iterator[Line]: """ StringTransformer instances have a call signature that mirrors that of the Transformer type. diff --git a/tests/data/preview/trailing_comma.py b/tests/data/preview/trailing_comma.py new file mode 100644 index 00000000000..5b09c664606 --- /dev/null +++ b/tests/data/preview/trailing_comma.py @@ -0,0 +1,55 @@ +e = { + "a": fun(msg, "ts"), + "longggggggggggggggid": ..., + "longgggggggggggggggggggkey": ..., "created": ... + # "longkey": ... +} +f = [ + arg1, + arg2, + arg3, arg4 + # comment +] +g = ( + arg1, + arg2, + arg3, arg4 + # comment +) +h = { + arg1, + arg2, + arg3, arg4 + # comment +} + +# output + +e = { + "a": fun(msg, "ts"), + "longggggggggggggggid": ..., + "longgggggggggggggggggggkey": ..., + "created": ..., + # "longkey": ... +} +f = [ + arg1, + arg2, + arg3, + arg4, + # comment +] +g = ( + arg1, + arg2, + arg3, + arg4, + # comment +) +h = { + arg1, + arg2, + arg3, + arg4, + # comment +} diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py index 5cb8763040a..64f5d09bbe8 100644 --- a/tests/data/preview_context_managers/targeting_py39.py +++ b/tests/data/preview_context_managers/targeting_py39.py @@ -84,7 +84,7 @@ # First comment. new_new_new1() as cm1, # Second comment. - new_new_new2() + new_new_new2(), # Last comment. ): pass From e506c46f7b44e788dfd279e9a474af83e4e03eca Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 4 Feb 2023 22:51:46 -0500 Subject: [PATCH 418/700] Rename design label to style because it's clearer (#3547) --- .github/ISSUE_TEMPLATE/style_issue.md | 6 +++--- docs/contributing/issue_triage.md | 4 ++-- docs/the_black_code_style/index.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/style_issue.md b/.github/ISSUE_TEMPLATE/style_issue.md index 2e4343a3527..a9ce85fd977 100644 --- a/.github/ISSUE_TEMPLATE/style_issue.md +++ b/.github/ISSUE_TEMPLATE/style_issue.md @@ -1,8 +1,8 @@ --- -name: Style issue -about: Help us improve the Black style +name: Code style issue +about: Help us improve the Black code style title: "" -labels: "T: design" +labels: "T: style" assignees: "" --- diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 865a47935ed..aa3e49a649b 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -1,6 +1,6 @@ # Issue triage -Currently, _Black_ uses the issue tracker for bugs, feature requests, proposed design +Currently, _Black_ uses the issue tracker for bugs, feature requests, proposed style modifications, and general user support. Each of these issues have to be triaged so they can be eventually be resolved somehow. This document outlines the triaging process and also the current guidelines and recommendations. @@ -53,7 +53,7 @@ The lifecycle of a bug report or user support issue typically goes something lik - the issue has been fixed - duplicate of another pre-existing issue or is invalid -For enhancement, documentation, and design issues, the lifecycle looks very similar but +For enhancement, documentation, and style issues, the lifecycle looks very similar but the details are different: 1. _the issue is waiting for triage_ diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md index e5967be2db4..1719347eec8 100644 --- a/docs/the_black_code_style/index.md +++ b/docs/the_black_code_style/index.md @@ -17,7 +17,7 @@ Python language and, occasionally, in response to user feedback. Large-scale sty 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](https://github.com/psf/black/labels/T%3A%20design) issue label. +[style](https://github.com/psf/black/labels/T%3A%20style) issue label. (labels/stability-policy)= From e74a05286b8b0c4a65419d21c4c17a33d9d7e15c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:56:38 -0800 Subject: [PATCH 419/700] Bump docker/build-push-action from 3 to 4 (#3549) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 04e30e727bd..8baace940ba 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,7 +36,7 @@ jobs: latest_non_release)" >> $GITHUB_ENV - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name == 'release' && github.event.action == 'published' && !github.event.release.prerelease }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 @@ -58,7 +58,7 @@ jobs: if: ${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 From 9c8464ca7ddd48d1c19112d895ae12d783f01563 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Tue, 7 Feb 2023 14:48:09 -0800 Subject: [PATCH 420/700] Fix typos in comments: assignement -> assignment (#3556) --- src/black/linegen.py | 2 +- tests/data/preview/prefer_rhs_split.py | 2 +- tests/data/simple_cases/prefer_rhs_split_reformatted.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index c582b2d6dff..f7d3655e962 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -762,7 +762,7 @@ def _maybe_split_omitting_optional_parens( # the split is right after `=` and len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL - # the left side of assignement contains brackets + # the left side of assignment contains brackets and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) # the left side of assignment is short enough (the -1 is for the ending # optional paren) diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/preview/prefer_rhs_split.py index 2f3cf33db41..a809eacc773 100644 --- a/tests/data/preview/prefer_rhs_split.py +++ b/tests/data/preview/prefer_rhs_split.py @@ -69,7 +69,7 @@ ] = lambda obj: obj.some_long_named_method() -# Make when when the left side of assignement plus the opening paren "... = (" is +# Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. xxxxxxxxx_yyy_zzzzzzzz[ xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) diff --git a/tests/data/simple_cases/prefer_rhs_split_reformatted.py b/tests/data/simple_cases/prefer_rhs_split_reformatted.py index 781e75be0aa..e15e5ddc86d 100644 --- a/tests/data/simple_cases/prefer_rhs_split_reformatted.py +++ b/tests/data/simple_cases/prefer_rhs_split_reformatted.py @@ -7,7 +7,7 @@ arg2, ) -# Make when when the left side of assignement plus the opening paren "... = (" is +# Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 @@ -31,7 +31,7 @@ arg2, ) -# Make when when the left side of assignement plus the opening paren "... = (" is +# Make when when the left side of assignment plus the opening paren "... = (" is # exactly line length limit + 1, it won't be split like that. xxxxxxxxx_yyy_zzzzzzzz[ xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) From d9b8a6407e2f46304a8d36b18e4a73d8e0613519 Mon Sep 17 00:00:00 2001 From: brucearctor <5032356+brucearctor@users.noreply.github.com> Date: Mon, 13 Feb 2023 17:24:28 -0800 Subject: [PATCH 421/700] Update Action example to use checkout@v3 (#3563) Latest version of `actions/checkout` is v3 (or rather v3.3) so let's use that in the example now. --- docs/integrations/github_actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index ebfcc2d95a2..56b2cdd0586 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -24,7 +24,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: psf/black@stable ``` From 25d886f52c2bbbb58386ac8050f4e67952507bc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 06:38:02 -0800 Subject: [PATCH 422/700] Bump peter-evans/find-comment from 2.2.0 to 2.3.0 (#3584) Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/peter-evans/find-comment/releases) - [Commits](https://github.com/peter-evans/find-comment/compare/81e2da3af01c92f83cb927cf3ace0e085617c556...034abe94d3191f9c89d870519735beae326f2bdb) --- updated-dependencies: - dependency-name: peter-evans/find-comment 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> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 26d06090919..af683cbb0ac 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -33,7 +33,7 @@ jobs: - name: Try to find pre-existing PR comment if: steps.metadata.outputs.needs-comment == 'true' id: find-comment - uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb with: issue-number: ${{ steps.metadata.outputs.pr-number }} comment-author: "github-actions[bot]" From 4a063a9f8d7069ea82186ac9aff5a2cd1c2618d7 Mon Sep 17 00:00:00 2001 From: Aneesh Agrawal Date: Tue, 7 Mar 2023 14:52:19 -0500 Subject: [PATCH 423/700] Improve multiline string handling (#1879) Co-authored-by: Olivia Hong Co-authored-by: Olivia Hong <24500729+olivia-hong@users.noreply.github.com> --- CHANGES.md | 1 + docs/the_black_code_style/future_style.md | 48 +++ src/black/linegen.py | 43 ++- src/black/lines.py | 96 +++++- src/black/mode.py | 1 + tests/data/preview/multiline_strings.py | 358 ++++++++++++++++++++++ 6 files changed, 514 insertions(+), 33 deletions(-) create mode 100644 tests/data/preview/multiline_strings.py diff --git a/CHANGES.md b/CHANGES.md index a8b556cb7e8..53682df2b39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -133,6 +133,7 @@ versions separately. code. Implicitly concatenated f-strings with different quotes can now be merged or quote-normalized by changing the quotes used in expressions. (#3509) - Fix crash on `await (yield)` when Black is compiled with mypyc (#3533) +- Improve handling of multiline strings by changing line split behavior (#1879) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index b2cdca2590d..96abc99ef41 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -111,3 +111,51 @@ my_dict = { "another key": short_value, } ``` + +### Improved multiline string handling + +_Black_ is smarter when formatting multiline strings, especially in function arguments, +to avoid introducing extra line breaks. Previously, it would always consider multiline +strings as not fitting on a single line. With this new feature, _Black_ looks at the +context around the multiline string to decide if it should be inlined or split to a +separate line. For example, when a multiline string is passed to a function, _Black_ +will only split the multiline string if a line is too long or if multiple arguments are +being passed. + +For example, _Black_ will reformat + +```python +textwrap.dedent( + """\ + This is a + multiline string +""" +) +``` + +to: + +```python +textwrap.dedent("""\ + This is a + multiline string +""") +``` + +And: + +```python +MULTILINE = """ +foobar +""".replace( + "\n", "" +) +``` + +to: + +```python +MULTILINE = """ +foobar +""".replace("\n", "") +``` diff --git a/src/black/linegen.py b/src/black/linegen.py index f7d3655e962..95d5583c5f5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,7 +2,7 @@ Generating lines of code. """ import sys -from dataclasses import dataclass +from dataclasses import dataclass, replace from enum import Enum, auto from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast @@ -505,7 +505,7 @@ def transform_line( and not line.should_split_rhs and not line.magic_trailing_comma and ( - is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) + is_line_short_enough(line, mode=mode, line_str=line_str) or line.contains_unsplittable_type_ignore() ) and not (line.inside_brackets and line.contains_standalone_comments()) @@ -529,14 +529,12 @@ def _rhs( bracket pair instead. """ for omit in generate_trailers_to_omit(line, mode.line_length): - lines = list( - right_hand_split(line, mode.line_length, features, omit=omit) - ) + lines = list(right_hand_split(line, mode, features, omit=omit)) # Note: this check is only able to figure out if the first line of the # *current* transformation fits in the line length. This is true only # for simple cases. All others require running more transforms via # `transform_line()`. This check doesn't know if those would succeed. - if is_line_short_enough(lines[0], line_length=mode.line_length): + if is_line_short_enough(lines[0], mode=mode): yield from lines return @@ -544,9 +542,7 @@ def _rhs( # This mostly happens to multiline strings that are by definition # reported as not fitting a single line, as well as lines that contain # trailing commas (those have to be exploded). - yield from right_hand_split( - line, line_length=mode.line_length, features=features - ) + yield from right_hand_split(line, mode, features=features) # HACK: nested functions (like _rhs) compiled by mypyc don't retain their # __name__ attribute which is needed in `run_transformer` further down. @@ -664,7 +660,7 @@ class _RHSResult: def right_hand_split( line: Line, - line_length: int, + mode: Mode, features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: @@ -678,7 +674,7 @@ def right_hand_split( """ rhs_result = _first_right_hand_split(line, omit=omit) yield from _maybe_split_omitting_optional_parens( - rhs_result, line, line_length, features=features, omit=omit + rhs_result, line, mode, features=features, omit=omit ) @@ -733,7 +729,7 @@ def _first_right_hand_split( def _maybe_split_omitting_optional_parens( rhs: _RHSResult, line: Line, - line_length: int, + mode: Mode, features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: @@ -751,7 +747,7 @@ def _maybe_split_omitting_optional_parens( # there are no standalone comments in the body and not rhs.body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(rhs.body, line_length) + and can_omit_invisible_parens(rhs.body, mode.line_length) ): omit = {id(rhs.closing_bracket), *omit} try: @@ -766,23 +762,24 @@ def _maybe_split_omitting_optional_parens( and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) # the left side of assignment is short enough (the -1 is for the ending # optional paren) - and is_line_short_enough(rhs.head, line_length=line_length - 1) + and is_line_short_enough( + rhs.head, mode=replace(mode, line_length=mode.line_length - 1) + ) # the left side of assignment won't explode further because of magic # trailing comma and rhs.head.magic_trailing_comma is None # the split by omitting optional parens isn't preferred by some other # reason - and not _prefer_split_rhs_oop(rhs_oop, line_length=line_length) + and not _prefer_split_rhs_oop(rhs_oop, mode) ): yield from _maybe_split_omitting_optional_parens( - rhs_oop, line, line_length, features=features, omit=omit + rhs_oop, line, mode, features=features, omit=omit ) return except CannotSplit as e: if not ( - can_be_split(rhs.body) - or is_line_short_enough(rhs.body, line_length=line_length) + can_be_split(rhs.body) or is_line_short_enough(rhs.body, mode=mode) ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." @@ -806,7 +803,7 @@ def _maybe_split_omitting_optional_parens( yield result -def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: +def _prefer_split_rhs_oop(rhs_oop: _RHSResult, mode: Mode) -> bool: """ Returns whether we should prefer the result from a split omitting optional parens. """ @@ -826,7 +823,7 @@ def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool: # the first line still contains the `=`) any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) # the first line is short enough - and is_line_short_enough(rhs_oop.head, line_length=line_length) + and is_line_short_enough(rhs_oop.head, mode=mode) ) # contains unsplittable type ignore or rhs_oop.head.contains_unsplittable_type_ignore() @@ -1525,7 +1522,7 @@ def run_transformer( or line.contains_multiline_strings() or result[0].contains_uncollapsable_type_comments() or result[0].contains_unsplittable_type_ignore() - or is_line_short_enough(result[0], line_length=mode.line_length) + or is_line_short_enough(result[0], mode=mode) # If any leaves have no parents (which _can_ occur since # `transform(line)` potentially destroys the line's underlying node # structure), then we can't proceed. Doing so would cause the below @@ -1540,8 +1537,6 @@ def run_transformer( second_opinion = run_transformer( line_copy, transform, mode, features_fop, line_str=line_str ) - if all( - is_line_short_enough(ln, line_length=mode.line_length) for ln in second_opinion - ): + if all(is_line_short_enough(ln, mode=mode) for ln in second_opinion): result = second_opinion return result diff --git a/src/black/lines.py b/src/black/lines.py index ec6ef5d9522..b65604864a4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1,4 +1,5 @@ import itertools +import math import sys from dataclasses import dataclass, field from typing import ( @@ -10,11 +11,12 @@ Sequence, Tuple, TypeVar, + Union, cast, ) from black.brackets import DOT_PRIORITY, BracketTracker -from black.mode import Mode +from black.mode import Mode, Preview from black.nodes import ( BRACKETS, CLOSING_BRACKETS, @@ -37,6 +39,7 @@ T = TypeVar("T") Index = int LeafID = int +LN = Union[Leaf, Node] @dataclass @@ -701,18 +704,93 @@ def append_leaves( new_line.append(comment_leaf, preformatted=True) -def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool: - """Return True if `line` is no longer than `line_length`. - +def is_line_short_enough( # noqa: C901 + line: Line, *, mode: Mode, line_str: str = "" +) -> bool: + """For non-multiline strings, return True if `line` is no longer than `line_length`. + For multiline strings, looks at the context around `line` to determine + if it should be inlined or split up. Uses the provided `line_str` rendering, if any, otherwise computes a new one. """ if not line_str: line_str = line_to_string(line) - return ( - len(line_str) <= line_length - and "\n" not in line_str # multiline strings - and not line.contains_standalone_comments() - ) + + if Preview.multiline_string_handling not in mode: + return ( + len(line_str) <= mode.line_length + and "\n" not in line_str # multiline strings + and not line.contains_standalone_comments() + ) + + if line.contains_standalone_comments(): + return False + if "\n" not in line_str: + # No multiline strings (MLS) present + return len(line_str) <= mode.line_length + + first, *_, last = line_str.split("\n") + if len(first) > mode.line_length or len(last) > mode.line_length: + return False + + # Traverse the AST to examine the context of the multiline string (MLS), + # tracking aspects such as depth and comma existence, + # to determine whether to split the MLS or keep it together. + # Depth (which is based on the existing bracket_depth concept) + # is needed to determine nesting level of the MLS. + # Includes special case for trailing commas. + commas: List[int] = [] # tracks number of commas per depth level + multiline_string: Optional[Leaf] = None + # store the leaves that contain parts of the MLS + multiline_string_contexts: List[LN] = [] + + max_level_to_update = math.inf # track the depth of the MLS + for i, leaf in enumerate(line.leaves): + if max_level_to_update == math.inf: + had_comma: Optional[int] = None + if leaf.bracket_depth + 1 > len(commas): + commas.append(0) + elif leaf.bracket_depth + 1 < len(commas): + had_comma = commas.pop() + if ( + had_comma is not None + and multiline_string is not None + and multiline_string.bracket_depth == leaf.bracket_depth + 1 + ): + # Have left the level with the MLS, stop tracking commas + max_level_to_update = leaf.bracket_depth + if had_comma > 0: + # MLS was in parens with at least one comma - force split + return False + + if leaf.bracket_depth <= max_level_to_update and leaf.type == token.COMMA: + # Ignore non-nested trailing comma + # directly after MLS/MLS-containing expression + ignore_ctxs: List[Optional[LN]] = [None] + ignore_ctxs += multiline_string_contexts + if not (leaf.prev_sibling in ignore_ctxs and i == len(line.leaves) - 1): + commas[leaf.bracket_depth] += 1 + if max_level_to_update != math.inf: + max_level_to_update = min(max_level_to_update, leaf.bracket_depth) + + if is_multiline_string(leaf): + if len(multiline_string_contexts) > 0: + # >1 multiline string cannot fit on a single line - force split + return False + multiline_string = leaf + ctx: LN = leaf + # fetch the leaf components of the MLS in the AST + while str(ctx) in line_str: + multiline_string_contexts.append(ctx) + if ctx.parent is None: + break + ctx = ctx.parent + + # May not have a triple-quoted multiline string at all, + # in case of a regular string with embedded newlines and line continuations + if len(multiline_string_contexts) == 0: + return True + + return all(val == 0 for val in commas) def can_be_split(line: Line) -> bool: diff --git a/src/black/mode.py b/src/black/mode.py index c9a4c2b080c..d70388916da 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -155,6 +155,7 @@ class Preview(Enum): add_trailing_comma_consistently = auto() hex_codes_in_unicode_sequences = auto() + multiline_string_handling = auto() prefer_splitting_right_hand_side_of_assignments = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. diff --git a/tests/data/preview/multiline_strings.py b/tests/data/preview/multiline_strings.py new file mode 100644 index 00000000000..bb517d128e2 --- /dev/null +++ b/tests/data/preview/multiline_strings.py @@ -0,0 +1,358 @@ +"""cow +say""", +call(3, "dogsay", textwrap.dedent("""dove + coo""" % "cowabunga")) +call(3, "dogsay", textwrap.dedent("""dove +coo""" % "cowabunga")) +call(3, textwrap.dedent("""cow + moo""" % "cowabunga"), "dogsay") +call(3, "dogsay", textwrap.dedent("""crow + caw""" % "cowabunga"),) +call(3, textwrap.dedent("""cat + meow""" % "cowabunga"), {"dog", "say"}) +call(3, {"dog", "say"}, textwrap.dedent("""horse + neigh""" % "cowabunga")) +call(3, {"dog", "say"}, textwrap.dedent("""pig + oink""" % "cowabunga"),) +textwrap.dedent("""A one-line triple-quoted string.""") +textwrap.dedent("""A two-line triple-quoted string +since it goes to the next line.""") +textwrap.dedent("""A three-line triple-quoted string +that not only goes to the next line +but also goes one line beyond.""") +textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. file contents. +""") +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. file contents. +""")) +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. {config_filename} file contents. +""".format("config_filename", config_filename))) +# Another use case +data = yaml.load("""\ +a: 1 +b: 2 +""") +data = yaml.load("""\ +a: 1 +b: 2 +""",) +data = yaml.load( + """\ + a: 1 + b: 2 +""" +) + +MULTILINE = """ +foo +""".replace("\n", "") +generated_readme = lambda project_name: """ +{} + + +""".strip().format(project_name) +parser.usage += """ +Custom extra help summary. + +Extra test: +- with +- bullets +""" + + +def get_stuff(cr, value): + # original + cr.execute(""" + SELECT whatever + FROM some_table t + WHERE id = %s + """, [value]) + return cr.fetchone() + + +def get_stuff(cr, value): + # preferred + cr.execute( + """ + SELECT whatever + FROM some_table t + WHERE id = %s + """, + [value], + ) + return cr.fetchone() + + +call(arg1, arg2, """ +short +""", arg3=True) +test_vectors = [ + "one-liner\n", + "two\nliner\n", + """expressed +as a three line +mulitline string""", +] + +_wat = re.compile( + r""" + regex + """, + re.MULTILINE | re.VERBOSE, +) +dis_c_instance_method = """\ +%3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE +""" % (_C.__init__.__code__.co_firstlineno + 1,) +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually {verb} the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. {file_type} file contents. +""".format(verb="using", file_type="test"))) +{"""cow +moos"""} +["""cow +moos"""] +["""cow +moos""", """dog +woofs +and +barks"""] +def dastardly_default_value( + cow: String = json.loads("""this +is +quite +the +dastadardly +value!"""), + **kwargs, +): + pass + +print(f""" + This {animal} + moos and barks +{animal} say +""") +msg = f"""The arguments {bad_arguments} were passed in. +Please use `--build-option` instead, +`--global-option` is reserved to flags like `--verbose` or `--quiet`. +""" + +# output +"""cow +say""", +call( + 3, + "dogsay", + textwrap.dedent("""dove + coo""" % "cowabunga"), +) +call( + 3, + "dogsay", + textwrap.dedent("""dove +coo""" % "cowabunga"), +) +call( + 3, + textwrap.dedent("""cow + moo""" % "cowabunga"), + "dogsay", +) +call( + 3, + "dogsay", + textwrap.dedent("""crow + caw""" % "cowabunga"), +) +call( + 3, + textwrap.dedent("""cat + meow""" % "cowabunga"), + {"dog", "say"}, +) +call( + 3, + {"dog", "say"}, + textwrap.dedent("""horse + neigh""" % "cowabunga"), +) +call( + 3, + {"dog", "say"}, + textwrap.dedent("""pig + oink""" % "cowabunga"), +) +textwrap.dedent("""A one-line triple-quoted string.""") +textwrap.dedent("""A two-line triple-quoted string +since it goes to the next line.""") +textwrap.dedent("""A three-line triple-quoted string +that not only goes to the next line +but also goes one line beyond.""") +textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. file contents. +""") +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. file contents. +""")) +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually leveraging the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. {config_filename} file contents. +""".format("config_filename", config_filename))) +# Another use case +data = yaml.load("""\ +a: 1 +b: 2 +""") +data = yaml.load( + """\ +a: 1 +b: 2 +""", +) +data = yaml.load("""\ + a: 1 + b: 2 +""") + +MULTILINE = """ +foo +""".replace("\n", "") +generated_readme = lambda project_name: """ +{} + + +""".strip().format(project_name) +parser.usage += """ +Custom extra help summary. + +Extra test: +- with +- bullets +""" + + +def get_stuff(cr, value): + # original + cr.execute( + """ + SELECT whatever + FROM some_table t + WHERE id = %s + """, + [value], + ) + return cr.fetchone() + + +def get_stuff(cr, value): + # preferred + cr.execute( + """ + SELECT whatever + FROM some_table t + WHERE id = %s + """, + [value], + ) + return cr.fetchone() + + +call( + arg1, + arg2, + """ +short +""", + arg3=True, +) +test_vectors = [ + "one-liner\n", + "two\nliner\n", + """expressed +as a three line +mulitline string""", +] + +_wat = re.compile( + r""" + regex + """, + re.MULTILINE | re.VERBOSE, +) +dis_c_instance_method = """\ +%3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE +""" % (_C.__init__.__code__.co_firstlineno + 1,) +path.write_text(textwrap.dedent("""\ + A triple-quoted string + actually {verb} the textwrap.dedent functionality + that ends in a trailing newline, + representing e.g. {file_type} file contents. +""".format(verb="using", file_type="test"))) +{"""cow +moos"""} +["""cow +moos"""] +[ + """cow +moos""", + """dog +woofs +and +barks""", +] + + +def dastardly_default_value( + cow: String = json.loads("""this +is +quite +the +dastadardly +value!"""), + **kwargs, +): + pass + + +print(f""" + This {animal} + moos and barks +{animal} say +""") +msg = f"""The arguments {bad_arguments} were passed in. +Please use `--build-option` instead, +`--global-option` is reserved to flags like `--verbose` or `--quiet`. +""" From d16a1dbd05832632d7f3be28f0a4b6e9b208807c Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 9 Mar 2023 22:01:20 -0800 Subject: [PATCH 424/700] Consistently wrap two context managers in parens (in --preview). (#3589) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + src/black/linegen.py | 26 ++++-------- src/black/lines.py | 42 +++++++++++++++++-- src/black/nodes.py | 10 +++++ .../auto_detect/features_3_8.py | 16 +++++++ .../targeting_py38.py | 16 +++++++ .../targeting_py39.py | 37 ++++++++++++++++ 7 files changed, 127 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 53682df2b39..2fa0cb41b38 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ - Add trailing commas to collection literals even if there's a comment after the last entry (#3393) +- `with` statements that contain two context managers will be consistently wrapped in + parentheses (#3589) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 95d5583c5f5..6f67799e717 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,7 +2,7 @@ Generating lines of code. """ import sys -from dataclasses import dataclass, replace +from dataclasses import replace from enum import Enum, auto from functools import partial, wraps from typing import Collection, Iterator, List, Optional, Set, Union, cast @@ -16,6 +16,7 @@ from black.comments import FMT_OFF, generate_comments, list_comments from black.lines import ( Line, + RHSResult, append_leaves, can_be_split, can_omit_invisible_parens, @@ -647,17 +648,6 @@ def left_hand_split( yield result -@dataclass -class _RHSResult: - """Intermediate split result from a right hand split.""" - - head: Line - body: Line - tail: Line - opening_bracket: Leaf - closing_bracket: Leaf - - def right_hand_split( line: Line, mode: Mode, @@ -681,7 +671,7 @@ def right_hand_split( def _first_right_hand_split( line: Line, omit: Collection[LeafID] = (), -) -> _RHSResult: +) -> RHSResult: """Split the line into head, body, tail starting with the last bracket pair. Note: this function should not have side effects. It's relied upon by @@ -723,11 +713,11 @@ def _first_right_hand_split( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) bracket_split_succeeded_or_raise(head, body, tail) - return _RHSResult(head, body, tail, opening_bracket, closing_bracket) + return RHSResult(head, body, tail, opening_bracket, closing_bracket) def _maybe_split_omitting_optional_parens( - rhs: _RHSResult, + rhs: RHSResult, line: Line, mode: Mode, features: Collection[Feature] = (), @@ -747,11 +737,11 @@ def _maybe_split_omitting_optional_parens( # there are no standalone comments in the body and not rhs.body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(rhs.body, mode.line_length) + and can_omit_invisible_parens(rhs, mode.line_length) ): omit = {id(rhs.closing_bracket), *omit} try: - # The _RHSResult Omitting Optional Parens. + # The RHSResult Omitting Optional Parens. rhs_oop = _first_right_hand_split(line, omit=omit) if not ( Preview.prefer_splitting_right_hand_side_of_assignments in line.mode @@ -803,7 +793,7 @@ def _maybe_split_omitting_optional_parens( yield result -def _prefer_split_rhs_oop(rhs_oop: _RHSResult, mode: Mode) -> bool: +def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool: """ Returns whether we should prefer the result from a split omitting optional parens. """ diff --git a/src/black/lines.py b/src/black/lines.py index b65604864a4..4b57d1f0ea8 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -15,7 +15,7 @@ cast, ) -from black.brackets import DOT_PRIORITY, BracketTracker +from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker from black.mode import Mode, Preview from black.nodes import ( BRACKETS, @@ -28,6 +28,7 @@ is_multiline_string, is_one_sequence_between, is_type_comment, + is_with_stmt, replace_child, syms, whitespace, @@ -122,6 +123,11 @@ def is_import(self) -> bool: """Is this an import line?""" return bool(self) and is_import(self.leaves[0]) + @property + def is_with_stmt(self) -> bool: + """Is this a with_stmt line?""" + return bool(self) and is_with_stmt(self.leaves[0]) + @property def is_class(self) -> bool: """Is this line a class definition?""" @@ -449,6 +455,17 @@ def __bool__(self) -> bool: return bool(self.leaves or self.comments) +@dataclass +class RHSResult: + """Intermediate split result from a right hand split.""" + + head: Line + body: Line + tail: Line + opening_bracket: Leaf + closing_bracket: Leaf + + @dataclass class LinesBlock: """Class that holds information about a block of formatted lines. @@ -830,25 +847,42 @@ def can_be_split(line: Line) -> bool: def can_omit_invisible_parens( - line: Line, + rhs: RHSResult, line_length: int, ) -> bool: - """Does `line` have a shape safe to reformat without optional parens around it? + """Does `rhs.body` have a shape safe to reformat without optional parens around it? Returns True for only a subset of potentially nice looking formattings but the point is to not return false positives that end up producing lines that are too long. """ + line = rhs.body bt = line.bracket_tracker if not bt.delimiters: # Without delimiters the optional parentheses are useless. return True max_priority = bt.max_delimiter_priority() - if bt.delimiter_count_with_priority(max_priority) > 1: + delimiter_count = bt.delimiter_count_with_priority(max_priority) + if delimiter_count > 1: # With more than one delimiter of a kind the optional parentheses read better. return False + if delimiter_count == 1: + if ( + Preview.wrap_multiple_context_managers_in_parens in line.mode + and max_priority == COMMA_PRIORITY + and rhs.head.is_with_stmt + ): + # For two context manager with statements, the optional parentheses read + # better. In this case, `rhs.body` is the context managers part of + # the with statement. `rhs.head` is the `with (` part on the previous + # line. + return False + # Otherwise it may also read better, but we don't do it today and requires + # careful considerations for all possible cases. See + # https://github.com/psf/black/issues/2156. + if max_priority == DOT_PRIORITY: # A single stranded method call doesn't require optional parentheses. return True diff --git a/src/black/nodes.py b/src/black/nodes.py index a588077f4de..90728f3c87c 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -789,6 +789,16 @@ def is_import(leaf: Leaf) -> bool: ) +def is_with_stmt(leaf: Leaf) -> bool: + """Return True if the given leaf starts a with statement.""" + return bool( + leaf.type == token.NAME + and leaf.value == "with" + and leaf.parent + and leaf.parent.type == syms.with_stmt + ) + + def is_type_comment(leaf: Leaf, suffix: str = "") -> bool: """Return True if the given leaf is a special comment. Only returns true for type comments for now.""" diff --git a/tests/data/preview_context_managers/auto_detect/features_3_8.py b/tests/data/preview_context_managers/auto_detect/features_3_8.py index e05094e1421..79e438b995e 100644 --- a/tests/data/preview_context_managers/auto_detect/features_3_8.py +++ b/tests/data/preview_context_managers/auto_detect/features_3_8.py @@ -16,6 +16,14 @@ pass +with mock.patch.object( + self.my_runner, "first_method", autospec=True +) as mock_run_adb, mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" +): + pass + + # output # This file doesn't use any Python 3.9+ only grammars. @@ -28,3 +36,11 @@ with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: pass + + +with mock.patch.object( + self.my_runner, "first_method", autospec=True +) as mock_run_adb, mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" +): + pass diff --git a/tests/data/preview_context_managers/targeting_py38.py b/tests/data/preview_context_managers/targeting_py38.py index 6ec4684e441..f125cdffb8a 100644 --- a/tests/data/preview_context_managers/targeting_py38.py +++ b/tests/data/preview_context_managers/targeting_py38.py @@ -23,6 +23,14 @@ pass +with mock.patch.object( + self.my_runner, "first_method", autospec=True +) as mock_run_adb, mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" +): + pass + + # output @@ -36,3 +44,11 @@ with new_new_new1() as cm1, new_new_new2(): pass + + +with mock.patch.object( + self.my_runner, "first_method", autospec=True +) as mock_run_adb, mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" +): + pass diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py index 64f5d09bbe8..643c6fd958b 100644 --- a/tests/data/preview_context_managers/targeting_py39.py +++ b/tests/data/preview_context_managers/targeting_py39.py @@ -49,6 +49,24 @@ pass +with mock.patch.object( + self.my_runner, "first_method", autospec=True +) as mock_run_adb, mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" +): + pass + + +with xxxxxxxx.some_kind_of_method( + some_argument=[ + "first", + "second", + "third", + ] +).another_method() as cmd: + pass + + # output @@ -102,3 +120,22 @@ ) as cm2, ): pass + + +with ( + mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb, + mock.patch.object( + self.my_runner, "second_method", autospec=True, return_value="foo" + ), +): + pass + + +with xxxxxxxx.some_kind_of_method( + some_argument=[ + "first", + "second", + "third", + ] +).another_method() as cmd: + pass From 6ffc5f7b01eab341f1b2cd373b6faed0072a2351 Mon Sep 17 00:00:00 2001 From: Casey Korver <84342833+Casey-Kiewit@users.noreply.github.com> Date: Sat, 11 Mar 2023 09:43:31 -0600 Subject: [PATCH 425/700] Correct spelling mistakes (#3599) --- autoload/black.vim | 2 +- docs/contributing/issue_triage.md | 2 +- src/blib2to3/pgen2/driver.py | 2 +- tests/data/preview/long_strings.py | 12 ++++++------ tests/data/simple_cases/comments4.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/autoload/black.vim b/autoload/black.vim index 5aec8725bd0..c682d51e2b0 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -9,7 +9,7 @@ def strtobool(text): return True if text.lower() in ['n', 'no', 'f', 'false', 'off', '0']: return False - raise ValueError(f"{text} is not convertable to boolean") + raise ValueError(f"{text} is not convertible to boolean") class Flag(collections.namedtuple("FlagBase", "name, cast")): @property diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index aa3e49a649b..89cfff76f7f 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -59,7 +59,7 @@ the details are different: 1. _the issue is waiting for triage_ 2. **identified** - has been marked with a type label and other relevant labels 3. **discussion** - the merits of the suggested changes are currently being discussed, a - PR would be acceptable but would be at sigificant risk of being rejected + PR would be acceptable but would be at significant risk of being rejected 4. **accepted & awaiting PR** - it's been determined the suggested changes are OK and a PR would be welcomed (`S: accepted`) 5. **closed**: - the issue has been resolved, reasons include: diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index daf271dfa9a..1741b33c510 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -107,7 +107,7 @@ def __next__(self) -> Any: def can_advance(self, to: int) -> bool: # Try to eat, fail if it can't. The eat operation is cached - # so there wont be any additional cost of eating here + # so there won't be any additional cost of eating here try: self.eat(to) except StopIteration: diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index b7a0a42f82a..c68da3a8632 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -26,15 +26,15 @@ ("This is a really long string that can't be expected to fit in one line and is used as a dict's key"): ["value1", "value2"], } -L1 = ["The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a list literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a list literal.", ("parens should be stripped for short string in list")] +L1 = ["The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a list literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a list literal.", ("parens should be stripped for short string in list")] L2 = ["This is a really long string that can't be expected to fit in one line and is the only child of a list literal."] -S1 = {"The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a set literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a set literal.", ("parens should be stripped for short string in set")} +S1 = {"The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a set literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a set literal.", ("parens should be stripped for short string in set")} S2 = {"This is a really long string that can't be expected to fit in one line and is the only child of a set literal."} -T1 = ("The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a tuple literal, so it's expected to be wrapped in parens when spliting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a tuple literal.", ("parens should be stripped for short string in list")) +T1 = ("The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a tuple literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a tuple literal.", ("parens should be stripped for short string in list")) T2 = ("This is a really long string that can't be expected to fit in one line and is the only child of a tuple literal.",) @@ -410,7 +410,7 @@ def foo(): ( "This is a really long string that can't possibly be expected to fit all" " together on one line. Also it is inside a list literal, so it's expected to" - " be wrapped in parens when spliting to avoid implicit str concatenation." + " be wrapped in parens when splitting to avoid implicit str concatenation." ), short_call("arg", {"key": "value"}), ( @@ -431,7 +431,7 @@ def foo(): ( "This is a really long string that can't possibly be expected to fit all" " together on one line. Also it is inside a set literal, so it's expected to be" - " wrapped in parens when spliting to avoid implicit str concatenation." + " wrapped in parens when splitting to avoid implicit str concatenation." ), short_call("arg", {"key": "value"}), ( @@ -452,7 +452,7 @@ def foo(): ( "This is a really long string that can't possibly be expected to fit all" " together on one line. Also it is inside a tuple literal, so it's expected to" - " be wrapped in parens when spliting to avoid implicit str concatenation." + " be wrapped in parens when splitting to avoid implicit str concatenation." ), short_call("arg", {"key": "value"}), ( diff --git a/tests/data/simple_cases/comments4.py b/tests/data/simple_cases/comments4.py index 2147d41c9da..9f4f39d8359 100644 --- a/tests/data/simple_cases/comments4.py +++ b/tests/data/simple_cases/comments4.py @@ -85,7 +85,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): return ( - # Standlone comment but weirdly placed. + # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar") .filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) From 71a2daaacf92d361c09bc7613023b285df83e7bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:30:04 -0700 Subject: [PATCH 426/700] Bump pypa/cibuildwheel from 2.11.4 to 2.12.1 (#3602) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.11.4 to 2.12.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.11.4...v2.12.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel 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> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 6b3eb903d84..d5797c7d230 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.11.4 + uses: pypa/cibuildwheel@v2.12.1 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From fc6cea0f0e0f6da99fffbedbfcf3d50cc0e641a4 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 16 Mar 2023 13:31:27 -0700 Subject: [PATCH 427/700] Consistently format async statements similar to their non-async version. (#3609) --- CHANGES.md | 2 ++ src/black/linegen.py | 19 +++++++++-- src/black/lines.py | 8 ++--- src/black/mode.py | 1 + src/black/nodes.py | 21 ++++++++++-- tests/data/preview/async_stmts.py | 27 +++++++++++++++ .../targeting_py39.py | 33 +++++++++++++++++++ 7 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 tests/data/preview/async_stmts.py diff --git a/CHANGES.md b/CHANGES.md index 2fa0cb41b38..eff2640a01e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ - Add trailing commas to collection literals even if there's a comment after the last entry (#3393) +- `async def`, `async for`, and `async with` statements are now formatted consistently + compared to their non-async version. (#3609) - `with` statements that contain two context managers will be consistently wrapped in parentheses (#3589) diff --git a/src/black/linegen.py b/src/black/linegen.py index 6f67799e717..b6b83da26f7 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -36,6 +36,7 @@ Visitor, ensure_visible, is_arith_like, + is_async_stmt_or_funcdef, is_atom_with_invisible_parens, is_docstring, is_empty_tuple, @@ -110,6 +111,17 @@ def line(self, indent: int = 0) -> Iterator[Line]: self.current_line.depth += indent return # Line is empty, don't emit. Creating a new one unnecessary. + if ( + Preview.improved_async_statements_handling in self.mode + and len(self.current_line.leaves) == 1 + and is_async_stmt_or_funcdef(self.current_line.leaves[0]) + ): + # Special case for async def/for/with statements. `visit_async_stmt` + # adds an `ASYNC` leaf then visits the child def/for/with statement + # nodes. Line yields from those nodes shouldn't treat the former + # `ASYNC` leaf as a complete line. + return + complete_line = self.current_line self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) yield complete_line @@ -301,8 +313,11 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]: break internal_stmt = next(children) - for child in internal_stmt.children: - yield from self.visit(child) + if Preview.improved_async_statements_handling in self.mode: + yield from self.visit(internal_stmt) + else: + for child in internal_stmt.children: + yield from self.visit(child) def visit_decorators(self, node: Node) -> Iterator[Line]: """Visit decorators.""" diff --git a/src/black/lines.py b/src/black/lines.py index 4b57d1f0ea8..329dfc4f0d3 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -28,7 +28,7 @@ is_multiline_string, is_one_sequence_between, is_type_comment, - is_with_stmt, + is_with_or_async_with_stmt, replace_child, syms, whitespace, @@ -124,9 +124,9 @@ def is_import(self) -> bool: return bool(self) and is_import(self.leaves[0]) @property - def is_with_stmt(self) -> bool: + def is_with_or_async_with_stmt(self) -> bool: """Is this a with_stmt line?""" - return bool(self) and is_with_stmt(self.leaves[0]) + return bool(self) and is_with_or_async_with_stmt(self.leaves[0]) @property def is_class(self) -> bool: @@ -872,7 +872,7 @@ def can_omit_invisible_parens( if ( Preview.wrap_multiple_context_managers_in_parens in line.mode and max_priority == COMMA_PRIORITY - and rhs.head.is_with_stmt + and rhs.head.is_with_or_async_with_stmt ): # For two context manager with statements, the optional parentheses read # better. In this case, `rhs.body` is the context managers part of diff --git a/src/black/mode.py b/src/black/mode.py index d70388916da..6af04179193 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -155,6 +155,7 @@ class Preview(Enum): add_trailing_comma_consistently = auto() hex_codes_in_unicode_sequences = auto() + improved_async_statements_handling = auto() multiline_string_handling = auto() prefer_splitting_right_hand_side_of_assignments = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens diff --git a/src/black/nodes.py b/src/black/nodes.py index 90728f3c87c..4e9411b1b79 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -789,13 +789,30 @@ def is_import(leaf: Leaf) -> bool: ) -def is_with_stmt(leaf: Leaf) -> bool: - """Return True if the given leaf starts a with statement.""" +def is_with_or_async_with_stmt(leaf: Leaf) -> bool: + """Return True if the given leaf starts a with or async with statement.""" return bool( leaf.type == token.NAME and leaf.value == "with" and leaf.parent and leaf.parent.type == syms.with_stmt + ) or bool( + leaf.type == token.ASYNC + and leaf.next_sibling + and leaf.next_sibling.type == syms.with_stmt + ) + + +def is_async_stmt_or_funcdef(leaf: Leaf) -> bool: + """Return True if the given leaf starts an async def/for/with statement. + + Note that `async def` can be either an `async_stmt` or `async_funcdef`, + the latter is used when it has decorators. + """ + return bool( + leaf.type == token.ASYNC + and leaf.parent + and leaf.parent.type in {syms.async_stmt, syms.async_funcdef} ) diff --git a/tests/data/preview/async_stmts.py b/tests/data/preview/async_stmts.py new file mode 100644 index 00000000000..fe9594b2164 --- /dev/null +++ b/tests/data/preview/async_stmts.py @@ -0,0 +1,27 @@ +async def func() -> (int): + return 0 + + +@decorated +async def func() -> (int): + return 0 + + +async for (item) in async_iter: + pass + + +# output + + +async def func() -> int: + return 0 + + +@decorated +async def func() -> int: + return 0 + + +async for item in async_iter: + pass diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py index 643c6fd958b..c9fcf9c8ba2 100644 --- a/tests/data/preview_context_managers/targeting_py39.py +++ b/tests/data/preview_context_managers/targeting_py39.py @@ -67,6 +67,23 @@ pass +async def func(): + async with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ + : + pass + + async with some_function( + argument1, argument2, argument3="some_value" + ) as some_cm, some_other_function( + argument1, argument2, argument3="some_value" + ): + pass + + # output @@ -139,3 +156,19 @@ ] ).another_method() as cmd: pass + + +async def func(): + async with ( + make_context_manager1() as cm1, + make_context_manager2() as cm2, + make_context_manager3() as cm3, + make_context_manager4() as cm4, + ): + pass + + async with ( + some_function(argument1, argument2, argument3="some_value") as some_cm, + some_other_function(argument1, argument2, argument3="some_value"), + ): + pass From 268dcb677ce80331e0ef104d8335c2ada32872fb Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 17 Mar 2023 21:39:21 -0700 Subject: [PATCH 428/700] Do not add an extra blank line to an import line that has fmt disabled (#3610) --- CHANGES.md | 3 +++ src/black/comments.py | 1 + src/black/lines.py | 21 +++++++++++++++++++++ src/blib2to3/pytree.py | 6 ++++++ tests/data/simple_cases/fmtonoff.py | 1 - tests/data/simple_cases/fmtpass_imports.py | 19 +++++++++++++++++++ 6 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/data/simple_cases/fmtpass_imports.py diff --git a/CHANGES.md b/CHANGES.md index eff2640a01e..06a0ab7e9eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ +- Import lines with `# fmt: skip` and `# fmt: off` no longer have an extra blank line + added when they are right after another import line (#3610) + ### Preview style diff --git a/src/black/comments.py b/src/black/comments.py index 7cf15bf67b3..619123ab4be 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -203,6 +203,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: STANDALONE_COMMENT, hidden_value, prefix=standalone_comment_prefix, + fmt_pass_converted_first_leaf=first_leaf_of(first), ), ) return True diff --git a/src/black/lines.py b/src/black/lines.py index 329dfc4f0d3..66bba14b357 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -195,6 +195,26 @@ def opens_block(self) -> bool: return False return self.leaves[-1].type == token.COLON + def is_fmt_pass_converted( + self, *, first_leaf_matches: Optional[Callable[[Leaf], bool]] = None + ) -> bool: + """Is this line converted from fmt off/skip code? + + If first_leaf_matches is not None, it only returns True if the first + leaf of converted code matches. + """ + if len(self.leaves) != 1: + return False + leaf = self.leaves[0] + if ( + leaf.type != STANDALONE_COMMENT + or leaf.fmt_pass_converted_first_leaf is None + ): + return False + return first_leaf_matches is None or first_leaf_matches( + leaf.fmt_pass_converted_first_leaf + ) + def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool: """If so, needs to be split before emitting.""" for leaf in self.leaves: @@ -597,6 +617,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: self.previous_line and self.previous_line.is_import and not current_line.is_import + and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import) and depth == self.previous_line.depth ): return (before or 1), 0 diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 15a1420ef7d..ea60c894e20 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -392,6 +392,10 @@ class Leaf(Base): _prefix = "" # Whitespace and comments preceding this token in the input lineno: int = 0 # Line where this token starts in the input column: int = 0 # Column where this token starts in the input + # If not None, this Leaf is created by converting a block of fmt off/skip + # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the + # converted code. + fmt_pass_converted_first_leaf: Optional["Leaf"] = None def __init__( self, @@ -401,6 +405,7 @@ def __init__( prefix: Optional[Text] = None, fixers_applied: List[Any] = [], opening_bracket: Optional["Leaf"] = None, + fmt_pass_converted_first_leaf: Optional["Leaf"] = None, ) -> None: """ Initializer. @@ -419,6 +424,7 @@ def __init__( self.fixers_applied: Optional[List[Any]] = fixers_applied[:] self.children = [] self.opening_bracket = opening_bracket + self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf def __repr__(self) -> str: """Return a canonical string representation.""" diff --git a/tests/data/simple_cases/fmtonoff.py b/tests/data/simple_cases/fmtonoff.py index e40ea2c8d21..d1f15cd5c8b 100644 --- a/tests/data/simple_cases/fmtonoff.py +++ b/tests/data/simple_cases/fmtonoff.py @@ -195,7 +195,6 @@ def single_literal_yapf_disable(): from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/tests/data/simple_cases/fmtpass_imports.py b/tests/data/simple_cases/fmtpass_imports.py new file mode 100644 index 00000000000..8b3c0bc662a --- /dev/null +++ b/tests/data/simple_cases/fmtpass_imports.py @@ -0,0 +1,19 @@ +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo From 34a93a8d49951828dacd4237feda5db1f6ec854b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Mar 2023 21:59:33 -0700 Subject: [PATCH 429/700] Bump peter-evans/create-or-update-comment from 2.1.0 to 2.1.1 (#3548) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/5adcb0bb0f9fb3f95ef05400558bdb3f329ee808...67dcc547d311b736a8e6c5c236542148a47adc3d) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index af683cbb0ac..bb81ca4f0d6 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -41,7 +41,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From c9efbf9d97b65d67f6e87ee4b77bed0445bd7a9f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 18 Mar 2023 10:41:48 -0700 Subject: [PATCH 430/700] Add SECURITY.md (#3612) --- CHANGES.md | 3 +++ SECURITY.md | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/CHANGES.md b/CHANGES.md index 06a0ab7e9eb..e2f21cf8f8a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -57,6 +57,9 @@ +- Document that only the most recent release is supported for security issues; + vulnerabilities should be reported through Tidelift (#3612) + ## 23.1.0 ### Highlights diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..47049501183 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Only the latest non-prerelease version is supported. + +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the +fix and disclosure. From a3e8247a41089175827a69e5413278ffdc28aff2 Mon Sep 17 00:00:00 2001 From: Mitch Negus <21086604+mitchnegus@users.noreply.github.com> Date: Sat, 18 Mar 2023 14:30:02 -0600 Subject: [PATCH 431/700] Update documentation regarding isort compatibility (#3567) --- docs/guides/using_black_with_other_tools.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 9356caaf0bd..6c6fbb88174 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -51,9 +51,9 @@ line_length = 88 #### Why those options above? -_Black_ wraps imports that surpass `line-length` by moving identifiers into their own -indented line. If that still doesn't fit the bill, it will put all of them in separate -lines and put a trailing comma. A more detailed explanation of this behaviour can be +_Black_ wraps imports that surpass `line-length` by moving identifiers onto separate +lines and by adding a trailing comma after each. A more detailed explanation of this +behaviour can be [found here](../the_black_code_style/current_style.md#how-black-wraps-lines). isort's default mode of wrapping imports that extend past the `line_length` limit is From d7a28dd78631fb962da95fb2d2de0e18ca6754a4 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sat, 18 Mar 2023 15:04:13 -0600 Subject: [PATCH 432/700] Enforce a blank line after a nested class in stubs (#3564) --- CHANGES.md | 2 ++ src/black/lines.py | 12 +++++++++--- src/black/mode.py | 1 + tests/data/miscellaneous/nested_class_stub.pyi | 16 ++++++++++++++++ tests/test_format.py | 6 ++++++ 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 tests/data/miscellaneous/nested_class_stub.pyi diff --git a/CHANGES.md b/CHANGES.md index e2f21cf8f8a..029d0bcbd3f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ compared to their non-async version. (#3609) - `with` statements that contain two context managers will be consistently wrapped in parentheses (#3589) +- For stubs, enforce one blank line after a nested class with a body other than just + `...` (#3564) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 66bba14b357..b2bdcc441b4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -521,7 +521,7 @@ class EmptyLineTracker: mode: Mode previous_line: Optional[Line] = None previous_block: Optional[LinesBlock] = None - previous_defs: List[int] = field(default_factory=list) + previous_defs: List[Line] = field(default_factory=list) semantic_leading_comment: Optional[LinesBlock] = None def maybe_empty_lines(self, current_line: Line) -> LinesBlock: @@ -577,12 +577,18 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: else: before = 0 depth = current_line.depth - while self.previous_defs and self.previous_defs[-1] >= depth: + while self.previous_defs and self.previous_defs[-1].depth >= depth: if self.mode.is_pyi: assert self.previous_line is not None if depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. before = min(1, before) + elif ( + Preview.blank_line_after_nested_stub_class in self.mode + and self.previous_defs[-1].is_class + and not self.previous_defs[-1].is_stub_class + ): + before = 1 elif depth: before = 0 else: @@ -637,7 +643,7 @@ def _maybe_empty_lines_for_class_or_def( self, current_line: Line, before: int ) -> Tuple[int, int]: if not current_line.is_decorator: - self.previous_defs.append(current_line.depth) + self.previous_defs.append(current_line) if self.previous_line is None: # Don't insert empty lines before the first line in the file. return 0, 0 diff --git a/src/black/mode.py b/src/black/mode.py index 6af04179193..0511676ce53 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -154,6 +154,7 @@ class Preview(Enum): """Individual preview style features.""" add_trailing_comma_consistently = auto() + blank_line_after_nested_stub_class = auto() hex_codes_in_unicode_sequences = auto() improved_async_statements_handling = auto() multiline_string_handling = auto() diff --git a/tests/data/miscellaneous/nested_class_stub.pyi b/tests/data/miscellaneous/nested_class_stub.pyi new file mode 100644 index 00000000000..daf281b517b --- /dev/null +++ b/tests/data/miscellaneous/nested_class_stub.pyi @@ -0,0 +1,16 @@ +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + class Inner: + inner_attr: int + outer_attr: int + +# output +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + + class Inner: + inner_attr: int + + outer_attr: int diff --git a/tests/test_format.py b/tests/test_format.py index ab849aac9a3..3a6cbc9e2e9 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -186,6 +186,12 @@ def test_stub() -> None: assert_format(source, expected, mode) +def test_nested_class_stub() -> None: + mode = replace(DEFAULT_MODE, is_pyi=True, preview=True) + source, expected = read_data("miscellaneous", "nested_class_stub.pyi") + assert_format(source, expected, mode) + + def test_power_op_newline() -> None: # requires line_length=0 source, expected = read_data("miscellaneous", "power_op_newline") From dba3c2695c59fdb11825dbdf8f3b0ab6e0b368b2 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Sun, 19 Mar 2023 07:43:39 -0700 Subject: [PATCH 433/700] Fix bug introduced in #3564. (#3615) --- src/black/lines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/lines.py b/src/black/lines.py index b2bdcc441b4..fb5933ecbfb 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -598,7 +598,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 1 elif ( not depth - and self.previous_defs[-1] + and self.previous_defs[-1].depth and current_line.leaves[-1].type == token.COLON and ( current_line.leaves[0].value From 53c23e62df9b182edf9e7ccf726acdcf8c25846f Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Mon, 20 Mar 2023 04:22:06 +0530 Subject: [PATCH 434/700] Support files with type comment syntax errors (#3594) --- CHANGES.md | 2 ++ src/black/parsing.py | 26 ++++++++++++++----- .../type_comment_syntax_error.py | 11 ++++++++ tests/test_format.py | 7 +++++ 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 tests/data/type_comments/type_comment_syntax_error.py diff --git a/CHANGES.md b/CHANGES.md index 029d0bcbd3f..f5c039f6509 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,8 @@ +- Added support for formatting files with invalid type comments (#3594) + ### Performance diff --git a/src/black/parsing.py b/src/black/parsing.py index ba474c5e047..eaa3c367e54 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -148,24 +148,29 @@ def lib2to3_unparse(node: Node) -> str: def parse_single_version( - src: str, version: Tuple[int, int] + src: str, version: Tuple[int, int], *, type_comments: bool ) -> Union[ast.AST, ast3.AST]: filename = "" # typed-ast is needed because of feature version limitations in the builtin ast 3.8> if sys.version_info >= (3, 8) and version >= (3,): - return ast.parse(src, filename, feature_version=version, type_comments=True) + return ast.parse( + src, filename, feature_version=version, type_comments=type_comments + ) if _IS_PYPY: # PyPy 3.7 doesn't support type comment tracking which is not ideal, but there's # not much we can do as typed-ast won't work either. if sys.version_info >= (3, 8): - return ast3.parse(src, filename, type_comments=True) + return ast3.parse(src, filename, type_comments=type_comments) else: return ast3.parse(src, filename) else: - # Typed-ast is guaranteed to be used here and automatically tracks type - # comments separately. - return ast3.parse(src, filename, feature_version=version[1]) + if type_comments: + # Typed-ast is guaranteed to be used here and automatically tracks type + # comments separately. + return ast3.parse(src, filename, feature_version=version[1]) + else: + return ast.parse(src, filename) def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: @@ -175,11 +180,18 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: first_error = "" for version in sorted(versions, reverse=True): try: - return parse_single_version(src, version) + return parse_single_version(src, version, type_comments=True) except SyntaxError as e: if not first_error: first_error = str(e) + # Try to parse without type comments + for version in sorted(versions, reverse=True): + try: + return parse_single_version(src, version, type_comments=False) + except SyntaxError: + pass + raise SyntaxError(first_error) diff --git a/tests/data/type_comments/type_comment_syntax_error.py b/tests/data/type_comments/type_comment_syntax_error.py new file mode 100644 index 00000000000..2e5ca2ede8c --- /dev/null +++ b/tests/data/type_comments/type_comment_syntax_error.py @@ -0,0 +1,11 @@ +def foo( + # type: Foo + x): pass + +# output + +def foo( + # type: Foo + x, +): + pass diff --git a/tests/test_format.py b/tests/test_format.py index 3a6cbc9e2e9..5a7b3bb6762 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -196,3 +196,10 @@ def test_power_op_newline() -> None: # requires line_length=0 source, expected = read_data("miscellaneous", "power_op_newline") assert_format(source, expected, mode=black.Mode(line_length=0)) + + +def test_type_comment_syntax_error() -> None: + """Test that black is able to format python code with type comment syntax errors.""" + source, expected = read_data("type_comments", "type_comment_syntax_error") + assert_format(source, expected) + black.assert_equivalent(source, expected) From 3a9d6f0a5f9013b97676f3d24246bd34d93fce4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:52:40 -0400 Subject: [PATCH 435/700] Bump myst-parser from 0.18.1 to 1.0.0 in /docs (#3601) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Richard Si --- docs/faq.md | 4 ++-- docs/requirements.txt | 2 +- docs/the_black_code_style/future_style.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index bc9deccb756..a6a422c2fec 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -23,7 +23,7 @@ hooks, and scripting `unexpand` to run after applying _Black_. ## Does Black have an API? Not yet. _Black_ is fundamentally a command line tool. Many -[integrations](integrations/index.rst) are provided, but a Python interface is not one +[integrations](/integrations/index.md) are provided, but a Python interface is not one of them. A simple API is being [planned](https://github.com/psf/black/issues/779) though. @@ -39,7 +39,7 @@ other tools, such as `# noqa`, may be moved by _Black_. See below for more detai ## How stable is Black's style? 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. +pragmatism. See [The Black Code Style](the_black_code_style/index.md) 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/requirements.txt b/docs/requirements.txt index 9a269d02a75..f683fd52355 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==0.18.1 +myst-parser==1.0.0 Sphinx==5.3.0 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 96abc99ef41..f5fc3644f18 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -51,7 +51,7 @@ with contextlib.ExitStack() as exit_stack: 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). Because the functionality is +as described in [The Black Code Style](index.md). Because the functionality is experimental, feedback and issue reports are highly encouraged! ### Improved string processing From 5c064a986c388e2be1e448c3e4b28e5f5ba7ce5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Mar 2023 19:00:14 -0400 Subject: [PATCH 436/700] Bump sphinx from 5.3.0 to 6.1.3 in /docs (#3499) 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 f683fd52355..2916b3cee6a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==1.0.0 -Sphinx==5.3.0 +Sphinx==6.1.3 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 From ef6e079901d53a42dfae4ab10b081ce7a73a47b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hong=20Minhee=20=28=E6=B4=AA=20=E6=B0=91=E6=86=99=29?= Date: Mon, 20 Mar 2023 08:09:57 +0900 Subject: [PATCH 437/700] Let string splitters respect `East_Asian_Width` property (#3445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch changes the preview style so that string splitters respect Unicode East Asian Width[^1] property. If you are not familiar to CJK languages it is not clear immediately. Let me elaborate with some examples. Traditionally, East Asian characters (including punctuation) have taken up space twice than European letters and stops when they are rendered in monospace typeset. Compare the following characters: ``` abcdefg. 글、字。 ``` The characters at the first line are half-width, and the second line are full-width. (Also note that the last character with a small circle, the East Asian period, is also full-width.) Therefore, if we want to prevent those full-width characters to exceed the maximum columns per line, we need to count their *width* rather than the number of characters. Again, the following characters: ``` 글、字。 ``` These are just 4 characters, but their total width is 8. Suppose we want to maintain up to 4 columns per line with the following text: ``` abcdefg. 글、字。 ``` How should it be then? We want it to look like: ``` abcd efg. 글、 字。 ``` However, Black currently turns it into like this: ``` abcd efg. 글、字。 ``` It's because Black currently counts the number of characters in the line instead of measuring their width. So, how could we measure the width? How can we tell if a character is full- or half-width? What if half-width characters and full-width ones are mixed in a line? That's why Unicode defined an attribute named `East_Asian_Width`. Unicode grouped every single character according to their width in fixed-width typeset. This partially addresses #1197, but only for string splitters. The other parts need to be fixed as well in future patches. This was implemented by copying rich's own approach to handling wide characters: generate a table using wcwidth, check it into source control, and use in to drive helper functions in Black's logic. This gets us the best of both worlds: accuracy and performance (and let's us update as per our stability policy too!). Co-authored-by: Jelle Zijlstra --- CHANGES.md | 5 + scripts/make_width_table.py | 73 +++ src/black/_width_table.py | 484 ++++++++++++++++++ src/black/lines.py | 9 +- src/black/strings.py | 55 ++ src/black/trans.py | 48 +- .../preview/long_strings__east_asian_width.py | 25 + 7 files changed, 678 insertions(+), 21 deletions(-) create mode 100644 scripts/make_width_table.py create mode 100644 src/black/_width_table.py create mode 100644 tests/data/preview/long_strings__east_asian_width.py diff --git a/CHANGES.md b/CHANGES.md index f5c039f6509..a429e32c8bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,11 @@ compared to their non-async version. (#3609) - `with` statements that contain two context managers will be consistently wrapped in parentheses (#3589) +- Let string splitters respect [East Asian Width](https://www.unicode.org/reports/tr11/) + (#3445) +- Now long string literals can be split after East Asian commas and periods (`、` U+3001 + IDEOGRAPHIC COMMA, `。` U+3002 IDEOGRAPHIC FULL STOP, & `,` U+FF0C FULLWIDTH COMMA) + besides before spaces (#3445) - For stubs, enforce one blank line after a nested class with a body other than just `...` (#3564) diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py new file mode 100644 index 00000000000..09aca9c34b5 --- /dev/null +++ b/scripts/make_width_table.py @@ -0,0 +1,73 @@ +"""Generates a width table for Unicode characters. + +This script generates a width table for Unicode characters that are not +narrow (width 1). The table is written to src/black/_width_table.py (note +that although this file is generated, it is checked into Git) and is used +by the char_width() function in src/black/strings.py. + +You should run this script when you upgrade wcwidth, which is expected to +happen when a new Unicode version is released. The generated table contains +the version of wcwidth and Unicode that it was generated for. + +In order to run this script, you need to install the latest version of wcwidth. +You can do this by running: + + pip install -U wcwidth + +""" +import sys +from os.path import basename, dirname, join +from typing import Iterable, Tuple + +import wcwidth + + +def make_width_table() -> Iterable[Tuple[int, int, int]]: + start_codepoint = -1 + end_codepoint = -1 + range_width = -2 + for codepoint in range(0, sys.maxunicode + 1): + width = wcwidth.wcwidth(chr(codepoint)) + if width <= 1: + # Ignore narrow characters along with zero-width characters so that + # they are treated as single-width. Note that treating zero-width + # characters as single-width is consistent with the heuristics built + # on top of str.isascii() in the str_width() function in strings.py. + continue + if start_codepoint < 0: + start_codepoint = codepoint + range_width = width + elif width != range_width or codepoint != end_codepoint + 1: + yield (start_codepoint, end_codepoint, range_width) + start_codepoint = codepoint + range_width = width + end_codepoint = codepoint + if start_codepoint >= 0: + yield (start_codepoint, end_codepoint, range_width) + + +def main() -> None: + table_path = join(dirname(__file__), "..", "src", "black", "_width_table.py") + with open(table_path, "w") as f: + f.write( + f"""# Generated by {basename(__file__)} +# wcwidth {wcwidth.__version__} +# Unicode {wcwidth.list_versions()[-1]} +import sys +from typing import List, Tuple + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + +WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [ +""" + ) + for triple in make_width_table(): + f.write(f" {triple!r},\n") + f.write("]\n") + + +if __name__ == "__main__": + main() diff --git a/src/black/_width_table.py b/src/black/_width_table.py new file mode 100644 index 00000000000..6923f597687 --- /dev/null +++ b/src/black/_width_table.py @@ -0,0 +1,484 @@ +# Generated by make_width_table.py +# wcwidth 0.2.6 +# Unicode 15.0.0 +import sys +from typing import List, Tuple + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + +WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [ + (0, 0, 0), + (1, 31, -1), + (127, 159, -1), + (768, 879, 0), + (1155, 1161, 0), + (1425, 1469, 0), + (1471, 1471, 0), + (1473, 1474, 0), + (1476, 1477, 0), + (1479, 1479, 0), + (1552, 1562, 0), + (1611, 1631, 0), + (1648, 1648, 0), + (1750, 1756, 0), + (1759, 1764, 0), + (1767, 1768, 0), + (1770, 1773, 0), + (1809, 1809, 0), + (1840, 1866, 0), + (1958, 1968, 0), + (2027, 2035, 0), + (2045, 2045, 0), + (2070, 2073, 0), + (2075, 2083, 0), + (2085, 2087, 0), + (2089, 2093, 0), + (2137, 2139, 0), + (2200, 2207, 0), + (2250, 2273, 0), + (2275, 2306, 0), + (2362, 2362, 0), + (2364, 2364, 0), + (2369, 2376, 0), + (2381, 2381, 0), + (2385, 2391, 0), + (2402, 2403, 0), + (2433, 2433, 0), + (2492, 2492, 0), + (2497, 2500, 0), + (2509, 2509, 0), + (2530, 2531, 0), + (2558, 2558, 0), + (2561, 2562, 0), + (2620, 2620, 0), + (2625, 2626, 0), + (2631, 2632, 0), + (2635, 2637, 0), + (2641, 2641, 0), + (2672, 2673, 0), + (2677, 2677, 0), + (2689, 2690, 0), + (2748, 2748, 0), + (2753, 2757, 0), + (2759, 2760, 0), + (2765, 2765, 0), + (2786, 2787, 0), + (2810, 2815, 0), + (2817, 2817, 0), + (2876, 2876, 0), + (2879, 2879, 0), + (2881, 2884, 0), + (2893, 2893, 0), + (2901, 2902, 0), + (2914, 2915, 0), + (2946, 2946, 0), + (3008, 3008, 0), + (3021, 3021, 0), + (3072, 3072, 0), + (3076, 3076, 0), + (3132, 3132, 0), + (3134, 3136, 0), + (3142, 3144, 0), + (3146, 3149, 0), + (3157, 3158, 0), + (3170, 3171, 0), + (3201, 3201, 0), + (3260, 3260, 0), + (3263, 3263, 0), + (3270, 3270, 0), + (3276, 3277, 0), + (3298, 3299, 0), + (3328, 3329, 0), + (3387, 3388, 0), + (3393, 3396, 0), + (3405, 3405, 0), + (3426, 3427, 0), + (3457, 3457, 0), + (3530, 3530, 0), + (3538, 3540, 0), + (3542, 3542, 0), + (3633, 3633, 0), + (3636, 3642, 0), + (3655, 3662, 0), + (3761, 3761, 0), + (3764, 3772, 0), + (3784, 3790, 0), + (3864, 3865, 0), + (3893, 3893, 0), + (3895, 3895, 0), + (3897, 3897, 0), + (3953, 3966, 0), + (3968, 3972, 0), + (3974, 3975, 0), + (3981, 3991, 0), + (3993, 4028, 0), + (4038, 4038, 0), + (4141, 4144, 0), + (4146, 4151, 0), + (4153, 4154, 0), + (4157, 4158, 0), + (4184, 4185, 0), + (4190, 4192, 0), + (4209, 4212, 0), + (4226, 4226, 0), + (4229, 4230, 0), + (4237, 4237, 0), + (4253, 4253, 0), + (4352, 4447, 2), + (4957, 4959, 0), + (5906, 5908, 0), + (5938, 5939, 0), + (5970, 5971, 0), + (6002, 6003, 0), + (6068, 6069, 0), + (6071, 6077, 0), + (6086, 6086, 0), + (6089, 6099, 0), + (6109, 6109, 0), + (6155, 6157, 0), + (6159, 6159, 0), + (6277, 6278, 0), + (6313, 6313, 0), + (6432, 6434, 0), + (6439, 6440, 0), + (6450, 6450, 0), + (6457, 6459, 0), + (6679, 6680, 0), + (6683, 6683, 0), + (6742, 6742, 0), + (6744, 6750, 0), + (6752, 6752, 0), + (6754, 6754, 0), + (6757, 6764, 0), + (6771, 6780, 0), + (6783, 6783, 0), + (6832, 6862, 0), + (6912, 6915, 0), + (6964, 6964, 0), + (6966, 6970, 0), + (6972, 6972, 0), + (6978, 6978, 0), + (7019, 7027, 0), + (7040, 7041, 0), + (7074, 7077, 0), + (7080, 7081, 0), + (7083, 7085, 0), + (7142, 7142, 0), + (7144, 7145, 0), + (7149, 7149, 0), + (7151, 7153, 0), + (7212, 7219, 0), + (7222, 7223, 0), + (7376, 7378, 0), + (7380, 7392, 0), + (7394, 7400, 0), + (7405, 7405, 0), + (7412, 7412, 0), + (7416, 7417, 0), + (7616, 7679, 0), + (8203, 8207, 0), + (8232, 8238, 0), + (8288, 8291, 0), + (8400, 8432, 0), + (8986, 8987, 2), + (9001, 9002, 2), + (9193, 9196, 2), + (9200, 9200, 2), + (9203, 9203, 2), + (9725, 9726, 2), + (9748, 9749, 2), + (9800, 9811, 2), + (9855, 9855, 2), + (9875, 9875, 2), + (9889, 9889, 2), + (9898, 9899, 2), + (9917, 9918, 2), + (9924, 9925, 2), + (9934, 9934, 2), + (9940, 9940, 2), + (9962, 9962, 2), + (9970, 9971, 2), + (9973, 9973, 2), + (9978, 9978, 2), + (9981, 9981, 2), + (9989, 9989, 2), + (9994, 9995, 2), + (10024, 10024, 2), + (10060, 10060, 2), + (10062, 10062, 2), + (10067, 10069, 2), + (10071, 10071, 2), + (10133, 10135, 2), + (10160, 10160, 2), + (10175, 10175, 2), + (11035, 11036, 2), + (11088, 11088, 2), + (11093, 11093, 2), + (11503, 11505, 0), + (11647, 11647, 0), + (11744, 11775, 0), + (11904, 11929, 2), + (11931, 12019, 2), + (12032, 12245, 2), + (12272, 12283, 2), + (12288, 12329, 2), + (12330, 12333, 0), + (12334, 12350, 2), + (12353, 12438, 2), + (12441, 12442, 0), + (12443, 12543, 2), + (12549, 12591, 2), + (12593, 12686, 2), + (12688, 12771, 2), + (12784, 12830, 2), + (12832, 12871, 2), + (12880, 19903, 2), + (19968, 42124, 2), + (42128, 42182, 2), + (42607, 42610, 0), + (42612, 42621, 0), + (42654, 42655, 0), + (42736, 42737, 0), + (43010, 43010, 0), + (43014, 43014, 0), + (43019, 43019, 0), + (43045, 43046, 0), + (43052, 43052, 0), + (43204, 43205, 0), + (43232, 43249, 0), + (43263, 43263, 0), + (43302, 43309, 0), + (43335, 43345, 0), + (43360, 43388, 2), + (43392, 43394, 0), + (43443, 43443, 0), + (43446, 43449, 0), + (43452, 43453, 0), + (43493, 43493, 0), + (43561, 43566, 0), + (43569, 43570, 0), + (43573, 43574, 0), + (43587, 43587, 0), + (43596, 43596, 0), + (43644, 43644, 0), + (43696, 43696, 0), + (43698, 43700, 0), + (43703, 43704, 0), + (43710, 43711, 0), + (43713, 43713, 0), + (43756, 43757, 0), + (43766, 43766, 0), + (44005, 44005, 0), + (44008, 44008, 0), + (44013, 44013, 0), + (44032, 55203, 2), + (63744, 64255, 2), + (64286, 64286, 0), + (65024, 65039, 0), + (65040, 65049, 2), + (65056, 65071, 0), + (65072, 65106, 2), + (65108, 65126, 2), + (65128, 65131, 2), + (65281, 65376, 2), + (65504, 65510, 2), + (66045, 66045, 0), + (66272, 66272, 0), + (66422, 66426, 0), + (68097, 68099, 0), + (68101, 68102, 0), + (68108, 68111, 0), + (68152, 68154, 0), + (68159, 68159, 0), + (68325, 68326, 0), + (68900, 68903, 0), + (69291, 69292, 0), + (69373, 69375, 0), + (69446, 69456, 0), + (69506, 69509, 0), + (69633, 69633, 0), + (69688, 69702, 0), + (69744, 69744, 0), + (69747, 69748, 0), + (69759, 69761, 0), + (69811, 69814, 0), + (69817, 69818, 0), + (69826, 69826, 0), + (69888, 69890, 0), + (69927, 69931, 0), + (69933, 69940, 0), + (70003, 70003, 0), + (70016, 70017, 0), + (70070, 70078, 0), + (70089, 70092, 0), + (70095, 70095, 0), + (70191, 70193, 0), + (70196, 70196, 0), + (70198, 70199, 0), + (70206, 70206, 0), + (70209, 70209, 0), + (70367, 70367, 0), + (70371, 70378, 0), + (70400, 70401, 0), + (70459, 70460, 0), + (70464, 70464, 0), + (70502, 70508, 0), + (70512, 70516, 0), + (70712, 70719, 0), + (70722, 70724, 0), + (70726, 70726, 0), + (70750, 70750, 0), + (70835, 70840, 0), + (70842, 70842, 0), + (70847, 70848, 0), + (70850, 70851, 0), + (71090, 71093, 0), + (71100, 71101, 0), + (71103, 71104, 0), + (71132, 71133, 0), + (71219, 71226, 0), + (71229, 71229, 0), + (71231, 71232, 0), + (71339, 71339, 0), + (71341, 71341, 0), + (71344, 71349, 0), + (71351, 71351, 0), + (71453, 71455, 0), + (71458, 71461, 0), + (71463, 71467, 0), + (71727, 71735, 0), + (71737, 71738, 0), + (71995, 71996, 0), + (71998, 71998, 0), + (72003, 72003, 0), + (72148, 72151, 0), + (72154, 72155, 0), + (72160, 72160, 0), + (72193, 72202, 0), + (72243, 72248, 0), + (72251, 72254, 0), + (72263, 72263, 0), + (72273, 72278, 0), + (72281, 72283, 0), + (72330, 72342, 0), + (72344, 72345, 0), + (72752, 72758, 0), + (72760, 72765, 0), + (72767, 72767, 0), + (72850, 72871, 0), + (72874, 72880, 0), + (72882, 72883, 0), + (72885, 72886, 0), + (73009, 73014, 0), + (73018, 73018, 0), + (73020, 73021, 0), + (73023, 73029, 0), + (73031, 73031, 0), + (73104, 73105, 0), + (73109, 73109, 0), + (73111, 73111, 0), + (73459, 73460, 0), + (73472, 73473, 0), + (73526, 73530, 0), + (73536, 73536, 0), + (73538, 73538, 0), + (78912, 78912, 0), + (78919, 78933, 0), + (92912, 92916, 0), + (92976, 92982, 0), + (94031, 94031, 0), + (94095, 94098, 0), + (94176, 94179, 2), + (94180, 94180, 0), + (94192, 94193, 2), + (94208, 100343, 2), + (100352, 101589, 2), + (101632, 101640, 2), + (110576, 110579, 2), + (110581, 110587, 2), + (110589, 110590, 2), + (110592, 110882, 2), + (110898, 110898, 2), + (110928, 110930, 2), + (110933, 110933, 2), + (110948, 110951, 2), + (110960, 111355, 2), + (113821, 113822, 0), + (118528, 118573, 0), + (118576, 118598, 0), + (119143, 119145, 0), + (119163, 119170, 0), + (119173, 119179, 0), + (119210, 119213, 0), + (119362, 119364, 0), + (121344, 121398, 0), + (121403, 121452, 0), + (121461, 121461, 0), + (121476, 121476, 0), + (121499, 121503, 0), + (121505, 121519, 0), + (122880, 122886, 0), + (122888, 122904, 0), + (122907, 122913, 0), + (122915, 122916, 0), + (122918, 122922, 0), + (123023, 123023, 0), + (123184, 123190, 0), + (123566, 123566, 0), + (123628, 123631, 0), + (124140, 124143, 0), + (125136, 125142, 0), + (125252, 125258, 0), + (126980, 126980, 2), + (127183, 127183, 2), + (127374, 127374, 2), + (127377, 127386, 2), + (127488, 127490, 2), + (127504, 127547, 2), + (127552, 127560, 2), + (127568, 127569, 2), + (127584, 127589, 2), + (127744, 127776, 2), + (127789, 127797, 2), + (127799, 127868, 2), + (127870, 127891, 2), + (127904, 127946, 2), + (127951, 127955, 2), + (127968, 127984, 2), + (127988, 127988, 2), + (127992, 128062, 2), + (128064, 128064, 2), + (128066, 128252, 2), + (128255, 128317, 2), + (128331, 128334, 2), + (128336, 128359, 2), + (128378, 128378, 2), + (128405, 128406, 2), + (128420, 128420, 2), + (128507, 128591, 2), + (128640, 128709, 2), + (128716, 128716, 2), + (128720, 128722, 2), + (128725, 128727, 2), + (128732, 128735, 2), + (128747, 128748, 2), + (128756, 128764, 2), + (128992, 129003, 2), + (129008, 129008, 2), + (129292, 129338, 2), + (129340, 129349, 2), + (129351, 129535, 2), + (129648, 129660, 2), + (129664, 129672, 2), + (129680, 129725, 2), + (129727, 129733, 2), + (129742, 129755, 2), + (129760, 129768, 2), + (129776, 129784, 2), + (131072, 196605, 2), + (196608, 262141, 2), + (917760, 917999, 0), +] diff --git a/src/black/lines.py b/src/black/lines.py index fb5933ecbfb..bf4c12cb684 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -33,6 +33,7 @@ syms, whitespace, ) +from black.strings import str_width from blib2to3.pgen2 import token from blib2to3.pytree import Leaf, Node @@ -759,9 +760,11 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) + width = str_width if mode.preview else len + if Preview.multiline_string_handling not in mode: return ( - len(line_str) <= mode.line_length + width(line_str) <= mode.line_length and "\n" not in line_str # multiline strings and not line.contains_standalone_comments() ) @@ -770,10 +773,10 @@ def is_line_short_enough( # noqa: C901 return False if "\n" not in line_str: # No multiline strings (MLS) present - return len(line_str) <= mode.line_length + return width(line_str) <= mode.line_length first, *_, last = line_str.split("\n") - if len(first) > mode.line_length or len(last) > mode.line_length: + if width(first) > mode.line_length or width(last) > mode.line_length: return False # Traverse the AST to examine the context of the multiline string (MLS), diff --git a/src/black/strings.py b/src/black/strings.py index 3e3bc12fe72..ac18aef51ed 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -14,6 +14,7 @@ else: from typing import Final +from black._width_table import WIDTH_TABLE STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters. STRING_PREFIX_RE: Final = re.compile( @@ -278,3 +279,57 @@ def replace(m: Match[str]) -> str: return back_slashes + "N{" + groups["N"].upper() + "}" leaf.value = re.sub(UNICODE_ESCAPE_RE, replace, text) + + +@lru_cache(maxsize=4096) +def char_width(char: str) -> int: + """Return the width of a single character as it would be displayed in a + terminal or editor (which respects Unicode East Asian Width). + + Full width characters are counted as 2, while half width characters are + counted as 1. Also control characters are counted as 0. + """ + table = WIDTH_TABLE + codepoint = ord(char) + highest = len(table) - 1 + lowest = 0 + idx = highest // 2 + while True: + start_codepoint, end_codepoint, width = table[idx] + if codepoint < start_codepoint: + highest = idx - 1 + elif codepoint > end_codepoint: + lowest = idx + 1 + else: + return 0 if width < 0 else width + if highest < lowest: + break + idx = (highest + lowest) // 2 + return 1 + + +def str_width(line_str: str) -> int: + """Return the width of `line_str` as it would be displayed in a terminal + or editor (which respects Unicode East Asian Width). + + You could utilize this function to determine, for example, if a string + is too wide to display in a terminal or editor. + """ + if line_str.isascii(): + # Fast path for a line consisting of only ASCII characters + return len(line_str) + return sum(map(char_width, line_str)) + + +def count_chars_in_width(line_str: str, max_width: int) -> int: + """Count the number of characters in `line_str` that would fit in a + terminal or editor of `max_width` (which respects Unicode East Asian + Width). + """ + total_width = 0 + for i, char in enumerate(line_str): + width = char_width(char) + if width + total_width > max_width: + return i + total_width += width + return len(line_str) diff --git a/src/black/trans.py b/src/black/trans.py index a6a416e71bc..95695f32b14 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -48,9 +48,11 @@ from black.rusty import Err, Ok, Result from black.strings import ( assert_is_leaf_string, + count_chars_in_width, get_string_prefix, has_triple_quotes, normalize_string_quotes, + str_width, ) from blib2to3.pgen2 import token from blib2to3.pytree import Leaf, Node @@ -71,6 +73,8 @@ class CannotTransform(Exception): TResult = Result[T, CannotTransform] # (T)ransform Result TMatchResult = TResult[List[Index]] +SPLIT_SAFE_CHARS = frozenset(["\u3001", "\u3002", "\uff0c"]) # East Asian stops + def TErr(err_msg: str) -> Err[CannotTransform]: """(T)ransform Err @@ -1164,7 +1168,7 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: # WMA4 the length of the inline comment. offset += len(comment_leaf.value) - max_string_length = self.line_length - offset + max_string_length = count_chars_in_width(str(line), self.line_length - offset) return max_string_length @staticmethod @@ -1419,11 +1423,13 @@ def maybe_append_string_operators(new_line: Line) -> None: is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA ) - def max_last_string() -> int: + def max_last_string_column() -> int: """ Returns: - The max allowed length of the string value used for the last - line we will construct. + The max allowed width of the string value used for the last + line we will construct. Note that this value means the width + rather than the number of characters (e.g., many East Asian + characters expand to two columns). """ result = self.line_length result -= line.depth * 4 @@ -1431,14 +1437,14 @@ def max_last_string() -> int: result -= string_op_leaves_length return result - # --- Calculate Max Break Index (for string value) + # --- Calculate Max Break Width (for string value) # We start with the line length limit - max_break_idx = self.line_length + max_break_width = self.line_length # The last index of a string of length N is N-1. - max_break_idx -= 1 + max_break_width -= 1 # Leading whitespace is not present in the string value (e.g. Leaf.value). - max_break_idx -= line.depth * 4 - if max_break_idx < 0: + max_break_width -= line.depth * 4 + if max_break_width < 0: yield TErr( f"Unable to split {LL[string_idx].value} at such high of a line depth:" f" {line.depth}" @@ -1451,7 +1457,7 @@ def max_last_string() -> int: # line limit. use_custom_breakpoints = bool( custom_splits - and all(csplit.break_idx <= max_break_idx for csplit in custom_splits) + and all(csplit.break_idx <= max_break_width for csplit in custom_splits) ) # Temporary storage for the remaining chunk of the string line that @@ -1467,7 +1473,7 @@ def more_splits_should_be_made() -> bool: if use_custom_breakpoints: return len(custom_splits) > 1 else: - return len(rest_value) > max_last_string() + return str_width(rest_value) > max_last_string_column() string_line_results: List[Ok[Line]] = [] while more_splits_should_be_made(): @@ -1477,7 +1483,10 @@ def more_splits_should_be_made() -> bool: break_idx = csplit.break_idx else: # Algorithmic Split (automatic) - max_bidx = max_break_idx - string_op_leaves_length + max_bidx = ( + count_chars_in_width(rest_value, max_break_width) + - string_op_leaves_length + ) maybe_break_idx = self._get_break_idx(rest_value, max_bidx) if maybe_break_idx is None: # If we are unable to algorithmically determine a good split @@ -1574,7 +1583,7 @@ def more_splits_should_be_made() -> bool: # Try to fit them all on the same line with the last substring... if ( - len(temp_value) <= max_last_string() + str_width(temp_value) <= max_last_string_column() or LL[string_idx + 1].type == token.COMMA ): last_line.append(rest_leaf) @@ -1694,6 +1703,7 @@ def passes_all_checks(i: Index) -> bool: section of this classes' docstring would be be met by returning @i. """ is_space = string[i] == " " + is_split_safe = is_valid_index(i - 1) and string[i - 1] in SPLIT_SAFE_CHARS is_not_escaped = True j = i - 1 @@ -1706,7 +1716,7 @@ def passes_all_checks(i: Index) -> bool: and len(string[:i]) >= self.MIN_SUBSTR_SIZE ) return ( - is_space + (is_space or is_split_safe) and is_not_escaped and is_big_enough and not breaks_unsplittable_expression(i) @@ -1851,11 +1861,13 @@ def do_splitter_match(self, line: Line) -> TMatchResult: if string_idx is not None: string_value = line.leaves[string_idx].value - # If the string has no spaces... - if " " not in string_value: + # If the string has neither spaces nor East Asian stops... + if not any( + char == " " or char in SPLIT_SAFE_CHARS for char in string_value + ): # And will still violate the line length limit when split... - max_string_length = self.line_length - ((line.depth + 1) * 4) - if len(string_value) > max_string_length: + max_string_width = self.line_length - ((line.depth + 1) * 4) + if str_width(string_value) > max_string_width: # And has no associated custom splits... if not self.has_custom_splits(string_value): # Then we should NOT put this string on its own line. diff --git a/tests/data/preview/long_strings__east_asian_width.py b/tests/data/preview/long_strings__east_asian_width.py new file mode 100644 index 00000000000..fb66a78ed8b --- /dev/null +++ b/tests/data/preview/long_strings__east_asian_width.py @@ -0,0 +1,25 @@ +# The following strings do not have not-so-many chars, but are long enough +# when these are rendered in a monospace font (if the renderer respects +# Unicode East Asian Width properties). +hangul = '코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열' +hanzi = '中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。' +japanese = 'コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列' + +# output + +# The following strings do not have not-so-many chars, but are long enough +# when these are rendered in a monospace font (if the renderer respects +# Unicode East Asian Width properties). +hangul = ( + "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이" + " 필요한 문자열" +) +hanzi = ( + "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長," + "因此需要換行的字符串。" +) +japanese = ( + "コードポイントの数は少ないが、" + "実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、" + "改行が要る文字列" +) From f3b1a3b9d2fc6de8f0845399cb80d8bdfd6400fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 07:15:43 -0400 Subject: [PATCH 438/700] Bump furo from 2022.12.7 to 2023.3.23 in /docs (#3624) 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 2916b3cee6a..9d059341b14 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==6.1.3 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.1 -furo==2022.12.7 +furo==2023.3.23 From b542f589a5c4041f54847591104cd51684849f2e Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Tue, 28 Mar 2023 03:40:27 +0200 Subject: [PATCH 439/700] Use GH action version when version argument not specified (#3543) --- .git_archival.txt | 4 ++++ .gitattributes | 1 + CHANGES.md | 3 +++ action/main.py | 24 +++++++++++++++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 00000000000..8fb235d7045 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..00a7b00c94e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/CHANGES.md b/CHANGES.md index a429e32c8bd..47c04a7bc76 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,6 +61,9 @@ +- Update GitHub Action to use the version of Black equivalent to action's version if + version input is not specified (#3543) + ### Documentation -- Import lines with `# fmt: skip` and `# fmt: off` no longer have an extra blank line - added when they are right after another import line (#3610) - ### Preview style -- Add trailing commas to collection literals even if there's a comment after the last - entry (#3393) -- `async def`, `async for`, and `async with` statements are now formatted consistently - compared to their non-async version. (#3609) -- `with` statements that contain two context managers will be consistently wrapped in - parentheses (#3589) -- Let string splitters respect [East Asian Width](https://www.unicode.org/reports/tr11/) - (#3445) -- Now long string literals can be split after East Asian commas and periods (`、` U+3001 - IDEOGRAPHIC COMMA, `。` U+3002 IDEOGRAPHIC FULL STOP, & `,` U+FF0C FULLWIDTH COMMA) - besides before spaces (#3445) -- For stubs, enforce one blank line after a nested class with a body other than just - `...` (#3564) - ### Configuration @@ -43,8 +26,6 @@ -- Added support for formatting files with invalid type comments (#3594) - ### Performance @@ -61,14 +42,57 @@ -- Update GitHub Action to use the version of Black equivalent to action's version if - version input is not specified (#3543) - ### Documentation +## 23.3.0 + +### Highlights + +This release fixes a longstanding confusing behavior in Black's GitHub action, where the +version of the action did not determine the version of Black being run (issue #3382). In +addition, there is a small bug fix around imports and a number of improvements to the +preview style. + +Please try out the +[preview style](https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html#preview-style) +with `black --preview` and tell us your feedback. All changes in the preview style are +expected to become part of Black's stable style in January 2024. + +### Stable style + +- Import lines with `# fmt: skip` and `# fmt: off` no longer have an extra blank line + added when they are right after another import line (#3610) + +### Preview style + +- Add trailing commas to collection literals even if there's a comment after the last + entry (#3393) +- `async def`, `async for`, and `async with` statements are now formatted consistently + compared to their non-async version. (#3609) +- `with` statements that contain two context managers will be consistently wrapped in + parentheses (#3589) +- Let string splitters respect [East Asian Width](https://www.unicode.org/reports/tr11/) + (#3445) +- Now long string literals can be split after East Asian commas and periods (`、` U+3001 + IDEOGRAPHIC COMMA, `。` U+3002 IDEOGRAPHIC FULL STOP, & `,` U+FF0C FULLWIDTH COMMA) + besides before spaces (#3445) +- For stubs, enforce one blank line after a nested class with a body other than just + `...` (#3564) + +### Parser + +- Added support for formatting files with invalid type comments (#3594) + +### Integrations + +- Update GitHub Action to use the version of Black equivalent to action's version if + version input is not specified (#3543) + +### Documentation + - Document that only the most recent release is supported for security issues; vulnerabilities should be reported through Tidelift (#3612) diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index d462e2cc18a..de521833609 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: 23.1.0 + rev: 23.3.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 2b41c187766..b101e179d0e 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -178,7 +178,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, version 23.1.0 +black, version 23.3.0 ``` An option to require a specific version to be running is also provided. From bf7a16254ec96b084a6caf3d435ec18f0f245cc7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Mar 2023 18:53:23 -0600 Subject: [PATCH 442/700] Fixup the changelog (#3628) --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 866d5489008..7c76bca4f6a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -81,6 +81,7 @@ expected to become part of Black's stable style in January 2024. besides before spaces (#3445) - For stubs, enforce one blank line after a nested class with a body other than just `...` (#3564) +- Improve handling of multiline strings by changing line split behavior (#1879) ### Parser @@ -90,6 +91,7 @@ expected to become part of Black's stable style in January 2024. - Update GitHub Action to use the version of Black equivalent to action's version if version input is not specified (#3543) +- Fix missing Python binary path in autoload script for vim (#3508) ### Documentation @@ -179,7 +181,6 @@ versions separately. code. Implicitly concatenated f-strings with different quotes can now be merged or quote-normalized by changing the quotes used in expressions. (#3509) - Fix crash on `await (yield)` when Black is compiled with mypyc (#3533) -- Improve handling of multiline strings by changing line split behavior (#1879) ### Configuration @@ -220,7 +221,6 @@ versions separately. - Move 3.11 CI to normal flow now that all dependencies support 3.11 (#3446) - Docker: Add new `latest_prerelease` tag automation to follow latest black alpha release on docker images (#3465) -- Fixed missing python binary path in autoload script for vim (#3508) ### Documentation From 96ee2fef3dd9fd53e290ab3237d6d5c4ff2bcc6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 06:55:17 -0700 Subject: [PATCH 443/700] Bump furo from 2023.3.23 to 2023.3.27 in /docs (#3636) Bumps [furo](https://github.com/pradyunsg/furo) from 2023.3.23 to 2023.3.27. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.03.23...2023.03.27) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-patch ... 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 9d059341b14..727a1512cec 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==6.1.3 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.1 -furo==2023.3.23 +furo==2023.3.27 From a552f7096a9f6e016c9bb1df1e0a77a17caeec1c Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Mon, 3 Apr 2023 22:56:59 +0900 Subject: [PATCH 444/700] Fix an example for 'Improved parentheses management' in the (future of the) Black code style (#3635) --- docs/the_black_code_style/future_style.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index f5fc3644f18..bfab905b288 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -93,7 +93,6 @@ parentheses are now removed. For example: ```python my_dict = { - my_dict = { "a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0, From f265ff5bcd0eafcde1e62df89fb8b62ad1439887 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 13 Apr 2023 17:12:05 -0700 Subject: [PATCH 445/700] Explicitly annotate this with `Final[str]` to make it work in mypyc 1.0.0+. (#3645) --- src/blib2to3/pgen2/tokenize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 257dbef4a19..a6353d154c9 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -425,7 +425,7 @@ def generate_tokens( logical line; continuation lines are included. """ lnum = parenlev = continued = 0 - numchars: Final = "0123456789" + numchars: Final[str] = "0123456789" contstr, needcont = "", 0 contline: Optional[str] = None indents = [0] From 02f81c6995db4688baedd3c63e4b9821c090f09c Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 14 Apr 2023 14:05:08 -0700 Subject: [PATCH 446/700] Fix two more mypyc issues with mypyc v1.2.0. (#3648) --- src/black/lines.py | 2 +- src/black/nodes.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index bf4c12cb684..9d33bfa10b4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -790,7 +790,7 @@ def is_line_short_enough( # noqa: C901 # store the leaves that contain parts of the MLS multiline_string_contexts: List[LN] = [] - max_level_to_update = math.inf # track the depth of the MLS + max_level_to_update: Union[int, float] = math.inf # track the depth of the MLS for i, leaf in enumerate(line.leaves): if max_level_to_update == math.inf: had_comma: Optional[int] = None diff --git a/src/black/nodes.py b/src/black/nodes.py index 4e9411b1b79..45070909df4 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -181,9 +181,9 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 `complex_subscript` signals whether the given leaf is part of a subscription which has non-trivial arguments, like arithmetic expressions or function calls. """ - NO: Final = "" - SPACE: Final = " " - DOUBLESPACE: Final = " " + NO: Final[str] = "" + SPACE: Final[str] = " " + DOUBLESPACE: Final[str] = " " t = leaf.type p = leaf.parent v = leaf.value From 3da73399552b17b70f59d23d32f2347d9a551188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 07:17:11 -0700 Subject: [PATCH 447/700] Bump sphinx-copybutton from 0.5.1 to 0.5.2 in /docs (#3651) Bumps [sphinx-copybutton](https://github.com/executablebooks/sphinx-copybutton) from 0.5.1 to 0.5.2. - [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.5.1...v0.5.2) --- updated-dependencies: - dependency-name: sphinx-copybutton dependency-type: direct:production update-type: version-update:semver-patch ... 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 727a1512cec..168f0c4ec91 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,5 +5,5 @@ Sphinx==6.1.3 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 -sphinx_copybutton==0.5.1 +sphinx_copybutton==0.5.2 furo==2023.3.27 From 4b76a548153ed5e58e61c90d0f2e4d817fc19537 Mon Sep 17 00:00:00 2001 From: James Braza Date: Wed, 19 Apr 2023 03:24:03 -0700 Subject: [PATCH 448/700] Document black-jupyter hook (#3650) Co-authored-by: Jelle Zijlstra --- docs/integrations/source_version_control.md | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index de521833609..8b8fd658e0e 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -14,7 +14,7 @@ repos: # supported by your project here, or alternatively use # pre-commit's default_language_version, see # https://pre-commit.com/#top_level-default_language_version - language_version: python3.9 + language_version: python3.11 ``` Feel free to switch out the `rev` value to something else, like another @@ -22,11 +22,27 @@ Feel free to switch out the `rev` value to something else, like another branches or other mutable refs since the hook [won't auto update as you may expect][pre-commit-mutable-rev]. -If you want support for Jupyter Notebooks as well, then replace `id: black` with -`id: black-jupyter`. +## Jupyter Notebooks + +There is an alternate hook `black-jupyter` that expands the targets of `black` to +include Jupyter Notebooks. To use this hook, simply replace the hook's `id: black` with +`id: black-jupyter` in the `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black-jupyter + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.11 +``` ```{note} -The `black-jupyter` hook is only available from version 21.8b0 and onwards. +The `black-jupyter` hook became available in version 21.8b0. ``` [black-tags]: https://github.com/psf/black/tags From de65741b8d49d78fa2675ef79b799cd35e92e7c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 04:46:34 -0700 Subject: [PATCH 449/700] Bump pypa/cibuildwheel from 2.12.1 to 2.12.3 (#3657) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.12.1 to 2.12.3. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.12.1...v2.12.3) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index d5797c7d230..4b59c481bb0 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.12.1 + uses: pypa/cibuildwheel@v2.12.3 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From e712e48e06420d9240ce95c81acfcf6f11d14c83 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Fri, 28 Apr 2023 11:10:01 -0700 Subject: [PATCH 450/700] Do not wrap implicitly concatenated strings used as func args in parens (#3640) --- CHANGES.md | 3 + src/black/__init__.py | 6 +- src/black/mode.py | 6 +- src/black/parsing.py | 8 +- src/black/trans.py | 43 ++++-- tests/data/preview/cantfit.py | 12 +- tests/data/preview/long_strings.py | 54 +++----- .../data/preview/long_strings__regression.py | 22 ++- tests/test_black.py | 126 ++++++------------ 9 files changed, 113 insertions(+), 167 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7c76bca4f6a..c7ecc396214 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Implicitly concatenated strings used as function args are no longer wrapped inside + parentheses (#3640) + ### Configuration diff --git a/src/black/__init__.py b/src/black/__init__.py index 4ebf28821c3..871e9a0d7c8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -503,10 +503,8 @@ def main( # noqa: C901 user_level_config = str(find_user_pyproject_toml()) if config == user_level_config: out( - ( - "Using configuration from user-level config at " - f"'{user_level_config}'." - ), + "Using configuration from user-level config at " + f"'{user_level_config}'.", fg="blue", ) elif config_source in ( diff --git a/src/black/mode.py b/src/black/mode.py index 0511676ce53..3e37a588e52 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -188,10 +188,8 @@ class Mode: def __post_init__(self) -> None: if self.experimental_string_processing: warn( - ( - "`experimental string processing` has been included in `preview`" - " and deprecated. Use `preview` instead." - ), + "`experimental string processing` has been included in `preview`" + " and deprecated. Use `preview` instead.", Deprecated, ) diff --git a/src/black/parsing.py b/src/black/parsing.py index eaa3c367e54..70ed99c1549 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -29,11 +29,9 @@ except ImportError: if sys.version_info < (3, 8) and not _IS_PYPY: print( - ( - "The typed_ast package is required but not installed.\n" - "You can upgrade to Python 3.8+ or install typed_ast with\n" - "`python3 -m pip install typed-ast`." - ), + "The typed_ast package is required but not installed.\n" + "You can upgrade to Python 3.8+ or install typed_ast with\n" + "`python3 -m pip install typed-ast`.", file=sys.stderr, ) sys.exit(1) diff --git a/src/black/trans.py b/src/black/trans.py index 95695f32b14..1e28ed0656e 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1186,19 +1186,33 @@ def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]: if LL[0].type != token.STRING: return None - # If the string is surrounded by commas (or is the first/last child)... - prev_sibling = LL[0].prev_sibling - next_sibling = LL[0].next_sibling - if not prev_sibling and not next_sibling and parent_type(LL[0]) == syms.atom: - # If it's an atom string, we need to check the parent atom's siblings. - parent = LL[0].parent - assert parent is not None # For type checkers. - prev_sibling = parent.prev_sibling - next_sibling = parent.next_sibling - if (not prev_sibling or prev_sibling.type == token.COMMA) and ( - not next_sibling or next_sibling.type == token.COMMA + matching_nodes = [ + syms.listmaker, + syms.dictsetmaker, + syms.testlist_gexp, + ] + # If the string is an immediate child of a list/set/tuple literal... + if ( + parent_type(LL[0]) in matching_nodes + or parent_type(LL[0].parent) in matching_nodes ): - return 0 + # And the string is surrounded by commas (or is the first/last child)... + prev_sibling = LL[0].prev_sibling + next_sibling = LL[0].next_sibling + if ( + not prev_sibling + and not next_sibling + and parent_type(LL[0]) == syms.atom + ): + # If it's an atom string, we need to check the parent atom's siblings. + parent = LL[0].parent + assert parent is not None # For type checkers. + prev_sibling = parent.prev_sibling + next_sibling = parent.next_sibling + if (not prev_sibling or prev_sibling.type == token.COMMA) and ( + not next_sibling or next_sibling.type == token.COMMA + ): + return 0 return None @@ -1811,8 +1825,9 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): * The line is an lambda expression and the value is a string. OR * The line starts with an "atom" string that prefers to be wrapped in - parens. It's preferred to be wrapped when the string is surrounded by - commas (or is the first/last child). + parens. It's preferred to be wrapped when it's is an immediate child of + a list/set/tuple literal, AND the string is surrounded by commas (or is + the first/last child). Transformations: The chosen string is wrapped in parentheses and then split at the LPAR. diff --git a/tests/data/preview/cantfit.py b/tests/data/preview/cantfit.py index cade382e30d..0849374f776 100644 --- a/tests/data/preview/cantfit.py +++ b/tests/data/preview/cantfit.py @@ -79,14 +79,10 @@ ) # long arguments normal_name = normal_function_name( - ( - "but with super long string arguments that on their own exceed the line limit" - " so there's no way it can ever fit" - ), - ( - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with" - " eggs with spam and eggs and spam with eggs" - ), + "but with super long string arguments that on their own exceed the line limit so" + " there's no way it can ever fit", + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" + " with spam and eggs and spam with eggs", this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, ) string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index c68da3a8632..059148729d5 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -323,10 +323,8 @@ def foo(): y = "Short string" print( - ( - "This is a really long string inside of a print statement with extra arguments" - " attached at the end of it." - ), + "This is a really long string inside of a print statement with extra arguments" + " attached at the end of it.", x, y, z, @@ -501,15 +499,13 @@ def foo(): ) bad_split_func1( - ( - "But what should happen when code has already " - "been formatted but in the wrong way? Like " - "with a space at the end instead of the " - "beginning. Or what about when it is split too " - "soon? In the case of a split that is too " - "short, black will try to honer the custom " - "split." - ), + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", xxx, yyy, zzz, @@ -612,11 +608,9 @@ def foo(): ) arg_comment_string = print( - ( # This comment gets thrown to the top. - "Long lines with inline comments which are apart of (and not the only member" - " of) an argument list should have their comments appended to the reformatted" - " string's enclosing left parentheses." - ), + "Long lines with inline comments which are apart of (and not the only member of) an" + " argument list should have their comments appended to the reformatted string's" + " enclosing left parentheses.", # This comment gets thrown to the top. "Arg #2", "Arg #3", "Arg #4", @@ -676,31 +670,23 @@ def foo(): ) func_with_bad_comma( - ( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", ) func_with_bad_comma( - ( # comment after comma - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", # comment after comma ) func_with_bad_comma( - ( - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", ) func_with_bad_comma( - ( # comment after comma - "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", # comment after comma ) func_with_bad_parens_that_wont_fit_in_one_line( diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index eead8c204a9..5f0646e6029 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -715,11 +715,9 @@ class A: def foo(): some_func_call( "xxxxxxxxxx", - ( - "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " - '"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; ' - "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" " - ), + "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " + '"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; ' + "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", None, ("xxxxxxxxxxx",), ), @@ -728,11 +726,9 @@ def foo(): class A: def foo(): some_func_call( - ( - "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " - "xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; " - "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" " - ), + "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " + "xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; " + "xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", None, ("xxxxxxxxxxx",), ), @@ -850,10 +846,8 @@ def foo(): ) lpar_and_rpar_have_comments = func_call( # LPAR Comment - ( # Comma Comment - "Long really ridiculous type of string that shouldn't really even exist at all." - " I mean commmme onnn!!!" - ), + "Long really ridiculous type of string that shouldn't really even exist at all. I" + " mean commmme onnn!!!", # Comma Comment ) # RPAR Comment cmd_fstring = ( diff --git a/tests/test_black.py b/tests/test_black.py index e5e17777715..00de5b745e7 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -567,10 +567,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - ( - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -579,10 +577,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -591,10 +587,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -603,10 +597,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "wat ignored: no match") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -615,28 +607,22 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "f4 already well formatted, good job.") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_report_quiet(self) -> None: @@ -678,10 +664,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - ( - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -689,10 +673,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 1) self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -701,10 +683,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -712,10 +692,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -723,28 +701,22 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_report_normal(self) -> None: @@ -788,10 +760,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - ( - "1 file reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -800,10 +770,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 1 file failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -812,10 +780,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -823,10 +789,8 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 2 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -834,28 +798,22 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - ( - "2 files reformatted, 3 files left unchanged, 2 files failed to" - " reformat." - ), + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) report.check = False report.diff = True self.assertEqual( unstyle(str(report)), - ( - "2 files would be reformatted, 3 files would be left unchanged, 2" - " files would fail to reformat." - ), + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat.", ) def test_lib2to3_parse(self) -> None: From a07871b9cd1b5a1469271be6aaac5766f5a0e0fc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 3 May 2023 08:43:20 -0700 Subject: [PATCH 451/700] Fix new mypy error in blib2to3 (#3674) See python/mypy#15174 --- src/blib2to3/pgen2/tokenize.py | 11 ++++++----- tests/data/simple_cases/fstring.py | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index a6353d154c9..82ac5130bad 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -163,7 +163,6 @@ def _combinations(*l): '"""': double3prog, **{f"{prefix}'''": single3prog for prefix in _strprefixes}, **{f'{prefix}"""': double3prog for prefix in _strprefixes}, - **{prefix: None for prefix in _strprefixes}, } triple_quoted: Final = ( @@ -599,11 +598,13 @@ def generate_tokens( ): if token[-1] == "\n": # continued string strstart = (lnum, start) - endprog = ( - endprogs[initial] - or endprogs[token[1]] - or endprogs[token[2]] + maybe_endprog = ( + endprogs.get(initial) + or endprogs.get(token[1]) + or endprogs.get(token[2]) ) + assert maybe_endprog is not None, f"endprog not found for {token}" + endprog = maybe_endprog contstr, needcont = line[start:], 1 contline = line break diff --git a/tests/data/simple_cases/fstring.py b/tests/data/simple_cases/fstring.py index 4b33231c01c..60560309376 100644 --- a/tests/data/simple_cases/fstring.py +++ b/tests/data/simple_cases/fstring.py @@ -7,6 +7,8 @@ f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" # output @@ -19,3 +21,5 @@ f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" From eb32729ab562c010d9f644a3bbb2305196b2deb7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 3 May 2023 10:26:57 -0700 Subject: [PATCH 452/700] blib2to3: add a few annotations (#3675) --- src/blib2to3/pgen2/tokenize.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 82ac5130bad..2d0cc4324ce 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -34,6 +34,7 @@ Iterator, List, Optional, + Set, Text, Tuple, Pattern, @@ -66,19 +67,19 @@ del token -def group(*choices): +def group(*choices: str) -> str: return "(" + "|".join(choices) + ")" -def any(*choices): +def any(*choices: str) -> str: return group(*choices) + "*" -def maybe(*choices): +def maybe(*choices: str) -> str: return group(*choices) + "?" -def _combinations(*l): +def _combinations(*l: str) -> Set[str]: return set(x + y for x in l for y in l + ("",) if x.casefold() != y.casefold()) @@ -187,15 +188,19 @@ class StopTokenizing(Exception): pass -def printtoken(type, token, xxx_todo_changeme, xxx_todo_changeme1, line): # for testing - (srow, scol) = xxx_todo_changeme - (erow, ecol) = xxx_todo_changeme1 +Coord = Tuple[int, int] + + +def printtoken( + type: int, token: Text, srow_col: Coord, erow_col: Coord, line: Text +) -> None: # for testing + (srow, scol) = srow_col + (erow, ecol) = erow_col print( "%d,%d-%d,%d:\t%s\t%s" % (srow, scol, erow, ecol, tok_name[type], repr(token)) ) -Coord = Tuple[int, int] TokenEater = Callable[[int, Text, Coord, Coord, Text], None] @@ -219,7 +224,7 @@ def tokenize(readline: Callable[[], Text], tokeneater: TokenEater = printtoken) # backwards compatible interface -def tokenize_loop(readline, tokeneater): +def tokenize_loop(readline: Callable[[], Text], tokeneater: TokenEater) -> None: for token_info in generate_tokens(readline): tokeneater(*token_info) @@ -229,7 +234,6 @@ def tokenize_loop(readline, tokeneater): class Untokenizer: - tokens: List[Text] prev_row: int prev_col: int @@ -603,7 +607,9 @@ def generate_tokens( or endprogs.get(token[1]) or endprogs.get(token[2]) ) - assert maybe_endprog is not None, f"endprog not found for {token}" + assert ( + maybe_endprog is not None + ), f"endprog not found for {token}" endprog = maybe_endprog contstr, needcont = line[start:], 1 contline = line @@ -632,7 +638,6 @@ def generate_tokens( if token in ("def", "for"): if stashed and stashed[0] == NAME and stashed[1] == "async": - if token == "def": async_def = True async_def_indent = indents[-1] From 64887aab032c0fd64f9238cdab6684f2fc0c7f33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 06:36:24 -0700 Subject: [PATCH 453/700] Bump peter-evans/create-or-update-comment from 2.1.1 to 3.0.1 (#3683) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 2.1.1 to 3.0.1. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/67dcc547d311b736a8e6c5c236542148a47adc3d...ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index bb81ca4f0d6..70ab7ff4f7a 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -41,7 +41,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From c97b9c55b488a478afe171537c0e4c0f10631ca1 Mon Sep 17 00:00:00 2001 From: Matthieu Simon Date: Mon, 15 May 2023 23:35:39 +0200 Subject: [PATCH 454/700] [github action] display black result in job summary (#3688) * send output to $GITHUB_STEP_SUMMARY * update CHANGES.md * update CHANGES.md with PR number * implement PR feedback * fix pre-commit issues (prettier/trailing whitespace) --- CHANGES.md | 2 ++ action.yml | 7 ++++--- action/main.py | 20 +++++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c7ecc396214..f9bec185ff5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,8 @@ +- Update GitHub Action to display black output in the job summary (#3688) + ### Documentation +- `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691) + ### Packaging diff --git a/src/black/const.py b/src/black/const.py index 0e13f31517d..ee466679c70 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|\.ipynb_checkpoints|_build|buck-out|build|dist|__pypackages__)/" # noqa: B950 +DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.ipynb_checkpoints|\.mypy_cache|\.nox|\.pytest_cache|\.ruff_cache|\.tox|\.svn|\.venv|\.vscode|__pypackages__|_build|buck-out|build|dist|venv)/" # noqa: B950 DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$" STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" From 2fd9d8b339e1e2e1b93956c6d68b2b358b3fc29d Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Fri, 19 May 2023 01:57:17 +0200 Subject: [PATCH 457/700] Remove blank lines before class docstring (#3692) --- CHANGES.md | 1 + src/black/lines.py | 2 + src/black/mode.py | 1 + .../preview/no_blank_line_before_docstring.py | 58 +++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 tests/data/preview/no_blank_line_before_docstring.py diff --git a/CHANGES.md b/CHANGES.md index a1bc7d59020..6a9923f8d8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ - Implicitly concatenated strings used as function args are no longer wrapped inside parentheses (#3640) +- Remove blank lines between a class definition and its docstring (#3692) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 9d33bfa10b4..daf0444d24e 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -634,6 +634,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: and self.previous_line.is_class and current_line.is_triple_quoted_string ): + if Preview.no_blank_line_before_class_docstring in current_line.mode: + return 0, 1 return before, 1 if self.previous_line and self.previous_line.opens_block: diff --git a/src/black/mode.py b/src/black/mode.py index 3e37a588e52..a5841edb30a 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -158,6 +158,7 @@ class Preview(Enum): hex_codes_in_unicode_sequences = auto() improved_async_statements_handling = auto() multiline_string_handling = auto() + no_blank_line_before_class_docstring = auto() prefer_splitting_right_hand_side_of_assignments = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. diff --git a/tests/data/preview/no_blank_line_before_docstring.py b/tests/data/preview/no_blank_line_before_docstring.py new file mode 100644 index 00000000000..a37362de100 --- /dev/null +++ b/tests/data/preview/no_blank_line_before_docstring.py @@ -0,0 +1,58 @@ +def line_before_docstring(): + + """Please move me up""" + + +class LineBeforeDocstring: + + """Please move me up""" + + +class EvenIfThereIsAMethodAfter: + + """I'm the docstring""" + def method(self): + pass + + +class TwoLinesBeforeDocstring: + + + """I want to be treated the same as if I were closer""" + + +class MultilineDocstringsAsWell: + + """I'm so far + + and on so many lines... + """ + + +# output + + +def line_before_docstring(): + """Please move me up""" + + +class LineBeforeDocstring: + """Please move me up""" + + +class EvenIfThereIsAMethodAfter: + """I'm the docstring""" + + def method(self): + pass + + +class TwoLinesBeforeDocstring: + """I want to be treated the same as if I were closer""" + + +class MultilineDocstringsAsWell: + """I'm so far + + and on so many lines... + """ From eedfc3832290b3a32825b3c0f2dfa3f3d7ee9d1c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 19 May 2023 13:00:29 -0400 Subject: [PATCH 458/700] Avoid EncodingWarning in blib2to3 (#3696) --- src/blib2to3/pgen2/pgen.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index 631682a77c9..b5ebc7b3e42 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -30,7 +30,6 @@ class PgenGrammar(grammar.Grammar): class ParserGenerator(object): - filename: Path stream: IO[Text] generator: Iterator[GoodTokenInfo] @@ -39,7 +38,7 @@ class ParserGenerator(object): def __init__(self, filename: Path, stream: Optional[IO[Text]] = None) -> None: close_stream = None if stream is None: - stream = open(filename) + stream = open(filename, encoding="utf-8") close_stream = stream.close self.filename = filename self.stream = stream From cd02c2809b193e17aa7c43f8dd0fae4695898184 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 11:47:58 -0400 Subject: [PATCH 459/700] Bump furo from 2023.3.27 to 2023.5.20 in /docs (#3698) 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 168f0c4ec91..7b26d089b01 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==6.1.3 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.2 -furo==2023.3.27 +furo==2023.5.20 From c99417ffe8aa51015f08a96220072fa0dbcce51d Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Wed, 24 May 2023 22:52:59 -0400 Subject: [PATCH 460/700] Change example from `%%writeline` to `%%writefile` (#3673) --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index a6a422c2fec..8941ca3fe4d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -57,8 +57,8 @@ _Black_ is timid about formatting Jupyter Notebooks. Cells containing any of the following will not be formatted: - automagics (e.g. `pip install black`) -- non-Python cell magics (e.g. `%%writeline`). These can be added with the flag - `--python-cell-magics`, e.g. `black --python-cell-magics writeline hello.ipynb`. +- non-Python cell magics (e.g. `%%writefile`). These can be added with the flag + `--python-cell-magics`, e.g. `black --python-cell-magics writefile hello.ipynb`. - multiline magics, e.g.: ```python From f95b43d6fa9883a87574f1d69d0a433422c19377 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Thu, 25 May 2023 04:53:27 +0200 Subject: [PATCH 461/700] docs: update note on GitHub .git-blame-ignore-revs support (#3655) --- docs/guides/introducing_black_to_your_project.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/guides/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md index 9ae40a1928e..53bb0d9fcd6 100644 --- a/docs/guides/introducing_black_to_your_project.md +++ b/docs/guides/introducing_black_to_your_project.md @@ -46,7 +46,5 @@ $ git config blame.ignoreRevsFile .git-blame-ignore-revs **The one caveat is that some online Git-repositories like GitLab do not yet support ignoring revisions using their native blame UI.** So blame information will be cluttered with a reformatting commit on those platforms. (If you'd like this feature, there's an -open issue for [GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423)). This is -however supported by -[GitHub](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view), -currently in beta. +open issue for [GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423)). +[GitHub supports `.git-blame-ignore-revs`](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view) by default in blame views however. From 3decbd6db9f120e8d7c8fa86b5b2f64f7861da0c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 24 May 2023 19:55:12 -0700 Subject: [PATCH 462/700] Document each configuration option in more detail (#2839) --- docs/the_black_code_style/current_style.md | 8 + docs/the_black_code_style/future_style.md | 2 + docs/usage_and_configuration/the_basics.md | 270 ++++++++++++++++----- scripts/check_version_in_basics_example.py | 17 +- 4 files changed, 230 insertions(+), 67 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 83f8785cc55..e2625f9e16e 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -140,6 +140,8 @@ If you're reaching for backslashes, that's a clear signal that you can do better slightly refactor your code. I hope some of the examples above show you that there are many ways in which you can do it. +(labels/line-length)= + ### Line length You probably noticed the peculiar default line length. _Black_ defaults to 88 characters @@ -273,6 +275,8 @@ A pre-existing trailing comma informs _Black_ to always explode contents of the bracket pair into one item per line. Read more about this in the [Pragmatism](#pragmatism) section below. +(labels/strings)= + ### Strings _Black_ prefers double quotes (`"` and `"""`) over single quotes (`'` and `'''`). It @@ -457,6 +461,8 @@ there were not many users anyway. Not many edge cases were reported. As a mature _Black_ does make some exceptions to rules it otherwise holds. This section documents what those exceptions are and why this is the case. +(labels/magic-trailing-comma)= + ### The magic trailing comma _Black_ in general does not take existing formatting into account. @@ -493,6 +499,8 @@ default by (among others) GitHub and Visual Studio Code, differentiates between r-strings and R-strings. The former are syntax highlighted as regular expressions while the latter are treated as true raw strings with no special semantics. +(labels/ast-changes)= + ### AST before and after formatting When run with `--safe` (the default), _Black_ checks that the code before and after is diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index bfab905b288..861bb64bff4 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -47,6 +47,8 @@ with contextlib.ExitStack() as exit_stack: ... ``` +(labels/preview-style)= + ## Preview style Experimental, potentially disruptive style changes are gathered under the `--preview` diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index b101e179d0e..48619c6bbe8 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -26,62 +26,106 @@ python -m black {source_file_or_directory} ### Command line options -The CLI options of _Black_ can be displayed by expanding the view below or by running -`black --help`. While _Black_ has quite a few knobs these days, it is still opinionated -so style options are deliberately limited and rarely added. +The CLI options of _Black_ can be displayed by running `black --help`. All options are +also covered in more detail below. -
+While _Black_ has quite a few knobs these days, it is still opinionated so style options +are deliberately limited and rarely added. -CLI reference +Note that all command-line options listed above can also be configured using a +`pyproject.toml` file (more on that below). -```{program-output} black --help +#### `-c`, `--code` +Format the code passed in as a string. + +```console +$ black --code "print ( 'hello, world' )" +print("hello, world") ``` -
+#### `-l`, `--line-length` -Note that all command-line options listed above can also be configured using a -`pyproject.toml` file (more on that below). +How many characters per line to allow. The default is 88. -### Code input alternatives +See also [the style documentation](labels/line-length). -#### Standard Input +#### `-t`, `--target-version` -_Black_ supports formatting code via stdin, with the result being printed to stdout. -Just let _Black_ know with `-` as the path. +Python versions that should be supported by Black's output. You should include all +versions that your code supports. If you support Python 3.7 through 3.10, you should +write: ```console -$ echo "print ( 'hello, world' )" | black - -print("hello, world") -reformatted - -All done! ✨ 🍰 ✨ -1 file reformatted. +$ black -t py37 -t py38 -t py39 -t py310 ``` -**Tip:** if you need _Black_ to treat stdin input as a file passed directly via the CLI, -use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude` -option on some editors that rely on using stdin. +In a [configuration file](#configuration-via-a-file), you can write: -#### As a string +```toml +target-versions = ["py37", "py38", "py39", "py310"] +``` -You can also pass code as a string using the `-c` / `--code` option. +_Black_ uses this option to decide what grammar to use to parse your code. In addition, +it may use it to decide what style to use. For example, support for a trailing comma +after `*args` in a function call was added in Python 3.5, so _Black_ will add this comma +only if the target versions are all Python 3.5 or higher: ```console -$ black --code "print ( 'hello, world' )" -print("hello, world") +$ black --line-length=10 --target-version=py35 -c 'f(a, *args)' +f( + a, + *args, +) +$ black --line-length=10 --target-version=py34 -c 'f(a, *args)' +f( + a, + *args +) +$ black --line-length=10 --target-version=py34 --target-version=py35 -c 'f(a, *args)' +f( + a, + *args +) ``` -### Writeback and reporting +#### `--pyi` -By default _Black_ reformats the files given and/or found in place. Sometimes you need -_Black_ to just tell you what it _would_ do without actually rewriting the Python files. +Format all input files like typing stubs regardless of file extension. This is useful +when piping source on standard input. -There's two variations to this mode that are independently enabled by their respective -flags. Both variations can be enabled at once. +#### `--ipynb` + +Format all input files like Jupyter Notebooks regardless of file extension. This is +useful when piping source on standard input. + +#### `--python-cell-magics` + +When processing Jupyter Notebooks, add the given magic to the list of known python- +magics. Useful for formatting cells with custom python magics. + +#### `-S, --skip-string-normalization` + +By default, _Black_ uses double quotes for all strings and normalizes string prefixes, +as described in [the style documentation](labels/strings). If this option is given, +strings are left unchanged instead. + +#### `-C, --skip-magic-trailing-comma` + +By default, _Black_ uses existing trailing commas as an indication that short lines +should be left separate, as described in +[the style documentation](labels/magic-trailing-comma). If this option is given, the +magic trailing comma is ignored. + +#### `--preview` + +Enable potentially disruptive style changes that may be added to Black's main +functionality in the next major release. Read more about +[our preview style](labels/preview-style). (labels/exit-code)= -#### Exit code +#### `--check` Passing `--check` will make _Black_ exit with: @@ -111,12 +155,12 @@ $ echo $? 123 ``` -#### Diffs +#### `--diff` Passing `--diff` will make _Black_ print out diffs that indicate what changes _Black_ would've made. They are printed to stdout so capturing them is simple. -If you'd like colored diffs, you can enable them with the `--color`. +If you'd like colored diffs, you can enable them with `--color`. ```console $ black test.py --diff @@ -130,22 +174,92 @@ All done! ✨ 🍰 ✨ 1 file would be reformatted. ``` -### Output verbosity +#### `--color` / `--no-color` -_Black_ in general tries to produce the right amount of output, balancing between -usefulness and conciseness. By default, _Black_ emits files modified and error messages, -plus a short summary. +Show (or do not show) colored diff. Only applies when `--diff` is given. + +#### `--fast` / `--safe` + +By default, _Black_ performs [an AST safety check](labels/ast-changes) after formatting +your code. The `--fast` flag turns off this check and the `--safe` flag explicitly +enables it. + +#### `--required-version` + +Require a specific version of _Black_ to be running. This is useful for ensuring that +all contributors to your project are using the same version, because different versions +of _Black_ may format code a little differently. This option can be set in a +configuration file for consistent results across environments. ```console -$ black src/ +$ black --version +black, 23.3.0 (compiled: yes) +$ black --required-version 23.3.0 -c "format = 'this'" +format = "this" +$ black --required-version 31.5b2 -c "still = 'beta?!'" +Oh no! 💥 💔 💥 The required version does not match the running version! +``` + +You can also pass just the major version: + +```console +$ black --required-version 22 -c "format = 'this'" +format = "this" +$ black --required-version 31 -c "still = 'beta?!'" +Oh no! 💥 💔 💥 The required version does not match the running version! +``` + +Because of our [stability policy](../the_black_code_style/index.md), this will guarantee +stable formatting, but still allow you to take advantage of improvements that do not +affect formatting. + +#### `--include` + +A regular expression that matches files and directories that should be included on +recursive searches. An empty value means all files are included regardless of the name. +Use forward slashes for directories on all platforms (Windows, too). Exclusions are +calculated first, inclusions later. + +#### `--exclude` + +A regular expression that matches files and directories that should be excluded on +recursive searches. An empty value means no paths are excluded. Use forward slashes for +directories on all platforms (Windows, too). Exclusions are calculated first, inclusions +later. + +#### `--extend-exclude` + +Like `--exclude`, but adds additional files and directories on top of the excluded ones. +Useful if you simply want to add to the default. + +#### `--force-exclude` + +Like `--exclude`, but files and directories matching this regex will be excluded even +when they are passed explicitly as arguments. This is useful when invoking _Black_ +programmatically on changed files, such as in a pre-commit hook or editor plugin. + +#### `--stdin-filename` + +The name of the file when passing it through stdin. Useful to make sure Black will +respect the `--force-exclude` option on some editors that rely on using stdin. + +#### `-W`, `--workers` + +When _Black_ formats multiple files, it may use a process pool to speed up formatting. +This option controls the number of parallel workers. + +#### `-q`, `--quiet` + +Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critical output. +Error messages will still be emitted (which can silenced by `2>/dev/null`). + +```console +$ black src/ -q error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio -reformatted src/black_primer/lib.py -reformatted src/blackd/__init__.py -reformatted src/black/__init__.py -Oh no! 💥 💔 💥 -3 files reformatted, 2 files left unchanged, 1 file failed to reformat. ``` +#### `-v`, `--verbose` + Passing `-v` / `--verbose` will cause _Black_ to also emit messages about files that were not changed or were ignored due to exclusion patterns. If _Black_ is using a configuration file, a blue message detailing which one it is using will be emitted. @@ -164,35 +278,73 @@ Oh no! 💥 💔 💥 3 files reformatted, 2 files left unchanged, 1 file failed to reformat ``` -Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critial output. -Error messages will still be emitted (which can silenced by `2>/dev/null`). +#### `--version` + +You can check the version of _Black_ you have installed using the `--version` flag. ```console -$ black src/ -q -error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio +$ black --version +black, 23.3.0 ``` -### Versions +#### `--config` -You can check the version of _Black_ you have installed using the `--version` flag. +Read configuration options from a configuration file. See +[below](#configuration-via-a-file) for more details on the configuration file. + +#### `-h`, `--help` + +Show available command-line options and exit. + +### Code input alternatives + +_Black_ supports formatting code via stdin, with the result being printed to stdout. +Just let _Black_ know with `-` as the path. ```console -$ black --version -black, version 23.3.0 +$ echo "print ( 'hello, world' )" | black - +print("hello, world") +reformatted - +All done! ✨ 🍰 ✨ +1 file reformatted. ``` -An option to require a specific version to be running is also provided. +**Tip:** if you need _Black_ to treat stdin input as a file passed directly via the CLI, +use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude` +option on some editors that rely on using stdin. + +You can also pass code as a string using the `-c` / `--code` option. + +### Writeback and reporting + +By default _Black_ reformats the files given and/or found in place. Sometimes you need +_Black_ to just tell you what it _would_ do without actually rewriting the Python files. + +There's two variations to this mode that are independently enabled by their respective +flags: + +- `--check` (exit with code 1 if any file would be reformatted) +- `--diff` (print a diff instead of reformatting files) + +Both variations can be enabled at once. + +### Output verbosity + +_Black_ in general tries to produce the right amount of output, balancing between +usefulness and conciseness. By default, _Black_ emits files modified and error messages, +plus a short summary. ```console -$ black --required-version 21.9b0 -c "format = 'this'" -format = "this" -$ black --required-version 31.5b2 -c "still = 'beta?!'" -Oh no! 💥 💔 💥 The required version does not match the running version! +$ black src/ +error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio +reformatted src/black_primer/lib.py +reformatted src/blackd/__init__.py +reformatted src/black/__init__.py +Oh no! 💥 💔 💥 +3 files reformatted, 2 files left unchanged, 1 file failed to reformat. ``` -This is useful for example when running _Black_ in multiple environments that haven't -necessarily installed the correct version. This option can be set in a configuration -file for consistent results across environments. +The `--quiet` and `--verbose` flags control output verbosity. ## Configuration via a file diff --git a/scripts/check_version_in_basics_example.py b/scripts/check_version_in_basics_example.py index c62780d97ab..7f559b3aee1 100644 --- a/scripts/check_version_in_basics_example.py +++ b/scripts/check_version_in_basics_example.py @@ -20,20 +20,21 @@ def main(changes: str, the_basics: str) -> None: the_basics_html = commonmark.commonmark(the_basics) the_basics_soup = BeautifulSoup(the_basics_html, "html.parser") - (version_example,) = [ + version_examples = [ code_block.string for code_block in the_basics_soup.find_all(class_="language-console") if "$ black --version" in code_block.string ] for tag in tags: - if tag in version_example and tag != latest_tag: - print( - "Please set the version in the ``black --version`` " - "example from ``the_basics.md`` to be the latest one.\n" - f"Expected {latest_tag}, got {tag}.\n" - ) - sys.exit(1) + for version_example in version_examples: + if tag in version_example and tag != latest_tag: + print( + "Please set the version in the ``black --version`` " + "examples from ``the_basics.md`` to be the latest one.\n" + f"Expected {latest_tag}, got {tag}.\n" + ) + sys.exit(1) if __name__ == "__main__": From c42178690e96f3bf061ad44f70dec52b1d8a299a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 24 May 2023 21:06:08 -0700 Subject: [PATCH 463/700] Fix docs formatting (#3704) --- docs/guides/introducing_black_to_your_project.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md index 53bb0d9fcd6..71a566fbda1 100644 --- a/docs/guides/introducing_black_to_your_project.md +++ b/docs/guides/introducing_black_to_your_project.md @@ -46,5 +46,6 @@ $ git config blame.ignoreRevsFile .git-blame-ignore-revs **The one caveat is that some online Git-repositories like GitLab do not yet support ignoring revisions using their native blame UI.** So blame information will be cluttered with a reformatting commit on those platforms. (If you'd like this feature, there's an -open issue for [GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423)). -[GitHub supports `.git-blame-ignore-revs`](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view) by default in blame views however. +open issue for [GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423)). +[GitHub supports `.git-blame-ignore-revs`](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view) +by default in blame views however. From a4032dce645b83e1faccc7274864869ddfe279fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 06:33:41 -0700 Subject: [PATCH 464/700] Bump pypa/cibuildwheel from 2.12.3 to 2.13.0 (#3710) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.12.3 to 2.13.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.12.3...v2.13.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel 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> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 4b59c481bb0..20787f71cee 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.12.3 + uses: pypa/cibuildwheel@v2.13.0 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From a538ab76636bbe71b7fbfeaf56fd8e61805df38f Mon Sep 17 00:00:00 2001 From: jmcb Date: Wed, 31 May 2023 22:29:31 +0100 Subject: [PATCH 465/700] blackd: show default values for options (#3712) * blackd: show default values for options Reference: https://click.palletsprojects.com/en/8.1.x/api/#click.Option * Fix spacing in CHANGES.md --- CHANGES.md | 3 +++ src/blackd/__init__.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6a9923f8d8d..762a799a1d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,9 @@ +- The `blackd` argument parser now shows the default values for options in their help + text (#3712) + ### Integrations diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index ba4750b8298..d331ad000bc 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -59,9 +59,15 @@ class InvalidVariantHeader(Exception): @click.command(context_settings={"help_option_names": ["-h", "--help"]}) @click.option( - "--bind-host", type=str, help="Address to bind the server to.", default="localhost" + "--bind-host", + type=str, + help="Address to bind the server to.", + default="localhost", + show_default=True, +) +@click.option( + "--bind-port", type=int, help="Port to listen on", default=45484, show_default=True ) -@click.option("--bind-port", type=int, help="Port to listen on", default=45484) @click.version_option(version=black.__version__) def main(bind_host: str, bind_port: int) -> None: logging.basicConfig(level=logging.INFO) From 3aad6e385bfbd4348b2e13695cb6741806951160 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 1 Jun 2023 18:37:08 -0700 Subject: [PATCH 466/700] Add support for PEP 695 syntax (#3703) --- CHANGES.md | 2 ++ pyproject.toml | 2 ++ src/black/__init__.py | 3 ++ src/black/linegen.py | 12 +++++++ src/black/mode.py | 21 ++++++++++++ src/blib2to3/Grammar.txt | 13 +++++-- src/blib2to3/pygram.py | 6 ++++ tests/data/py_312/type_aliases.py | 13 +++++++ tests/data/py_312/type_params.py | 57 +++++++++++++++++++++++++++++++ tests/test_black.py | 30 +++++++++++++--- tests/test_format.py | 7 ++++ 11 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 tests/data/py_312/type_aliases.py create mode 100644 tests/data/py_312/type_params.py diff --git a/CHANGES.md b/CHANGES.md index 762a799a1d1..fb3dea8c348 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,8 @@ +- Add support for the new PEP 695 syntax in Python 3.12 (#3703) + ### Performance diff --git a/pyproject.toml b/pyproject.toml index 435626ac8f4..6803a627e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,4 +214,6 @@ filterwarnings = [ # aiohttp is using deprecated cgi modules - Safe to remove when fixed: # https://github.com/aio-libs/aiohttp/issues/6905 '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', + # Work around https://github.com/pytest-dev/pytest/issues/10977 for Python 3.12 + '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''' ] diff --git a/src/black/__init__.py b/src/black/__init__.py index 871e9a0d7c8..8a759aa493a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1275,6 +1275,9 @@ def get_features_used( # noqa: C901 ): features.add(Feature.VARIADIC_GENERICS) + elif n.type in (syms.type_stmt, syms.typeparams): + features.add(Feature.TYPE_PARAMS) + return features diff --git a/src/black/linegen.py b/src/black/linegen.py index b6b83da26f7..0091cbb3bd1 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -215,6 +215,18 @@ def visit_stmt( yield from self.visit(child) + def visit_typeparams(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[0].prefix = "" + + def visit_typevartuple(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[1].prefix = "" + + def visit_paramspec(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[1].prefix = "" + def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if Preview.wrap_long_dict_values_in_parens in self.mode: for i, child in enumerate(node.children): diff --git a/src/black/mode.py b/src/black/mode.py index a5841edb30a..1091494afac 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -30,6 +30,7 @@ class TargetVersion(Enum): PY39 = 9 PY310 = 10 PY311 = 11 + PY312 = 12 class Feature(Enum): @@ -51,6 +52,7 @@ class Feature(Enum): VARIADIC_GENERICS = 15 DEBUG_F_STRINGS = 16 PARENTHESIZED_CONTEXT_MANAGERS = 17 + TYPE_PARAMS = 18 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -143,6 +145,25 @@ class Feature(Enum): Feature.EXCEPT_STAR, Feature.VARIADIC_GENERICS, }, + TargetVersion.PY312: { + Feature.F_STRINGS, + Feature.DEBUG_F_STRINGS, + Feature.NUMERIC_UNDERSCORES, + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.ASYNC_KEYWORDS, + Feature.FUTURE_ANNOTATIONS, + Feature.ASSIGNMENT_EXPRESSIONS, + Feature.RELAXED_DECORATORS, + Feature.POS_ONLY_ARGUMENTS, + Feature.UNPACKING_ON_FLOW, + Feature.ANN_ASSIGN_EXTENDED_RHS, + Feature.PARENTHESIZED_CONTEXT_MANAGERS, + Feature.PATTERN_MATCHING, + Feature.EXCEPT_STAR, + Feature.VARIADIC_GENERICS, + Feature.TYPE_PARAMS, + }, } diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index bd8a452a386..e48e66363fb 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -12,11 +12,17 @@ file_input: (NEWLINE | stmt)* ENDMARKER single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE eval_input: testlist NEWLINE* ENDMARKER +typevar: NAME [':' expr] +paramspec: '**' NAME +typevartuple: '*' NAME +typeparam: typevar | paramspec | typevartuple +typeparams: '[' typeparam (',' typeparam)* [','] ']' + decorator: '@' namedexpr_test NEWLINE decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: ASYNC funcdef -funcdef: 'def' NAME parameters ['->' test] ':' suite +funcdef: 'def' NAME [typeparams] parameters ['->' test] ':' suite parameters: '(' [typedargslist] ')' # The following definition for typedarglist is equivalent to this set of rules: @@ -74,7 +80,7 @@ vfplist: vfpdef (',' vfpdef)* [','] stmt: simple_stmt | compound_stmt simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE -small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | +small_stmt: (type_stmt | expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | exec_stmt | assert_stmt) expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) | ('=' (yield_expr|testlist_star_expr))*) @@ -105,6 +111,7 @@ dotted_name: NAME ('.' NAME)* global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* exec_stmt: 'exec' expr ['in' test [',' test]] assert_stmt: 'assert' test [',' test] +type_stmt: "type" NAME [typeparams] '=' expr compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt async_stmt: ASYNC (funcdef | with_stmt | for_stmt) @@ -174,7 +181,7 @@ dictsetmaker: ( ((test ':' asexpr_test | '**' expr) ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite +classdef: 'class' NAME [typeparams] ['(' [arglist] ')'] ':' suite arglist: argument (',' argument)* [','] diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index 99012cdd9cb..15702e4059e 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -95,6 +95,7 @@ class _python_symbols(Symbols): old_test: int or_test: int parameters: int + paramspec: int pass_stmt: int pattern: int patterns: int @@ -126,7 +127,12 @@ class _python_symbols(Symbols): tname_star: int trailer: int try_stmt: int + type_stmt: int typedargslist: int + typeparam: int + typeparams: int + typevar: int + typevartuple: int varargslist: int vfpdef: int vfplist: int diff --git a/tests/data/py_312/type_aliases.py b/tests/data/py_312/type_aliases.py new file mode 100644 index 00000000000..84e07e50fe2 --- /dev/null +++ b/tests/data/py_312/type_aliases.py @@ -0,0 +1,13 @@ +type A=int +type Gen[T]=list[T] + +type = aliased +print(type(42)) + +# output + +type A = int +type Gen[T] = list[T] + +type = aliased +print(type(42)) diff --git a/tests/data/py_312/type_params.py b/tests/data/py_312/type_params.py new file mode 100644 index 00000000000..5f8ec43267c --- /dev/null +++ b/tests/data/py_312/type_params.py @@ -0,0 +1,57 @@ +def func [T ](): pass +async def func [ T ] (): pass +class C[ T ] : pass + +def all_in[T : int,U : (bytes, str),* Ts,**P](): pass + +def really_long[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine](): pass + +def even_longer[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound](): pass + +def it_gets_worse[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, ItCouldBeGenericOverMultipleTypeVars](): pass + +def magic[Trailing, Comma,](): pass + +# output + + +def func[T](): + pass + + +async def func[T](): + pass + + +class C[T]: + pass + + +def all_in[T: int, U: (bytes, str), *Ts, **P](): + pass + + +def really_long[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine +](): + pass + + +def even_longer[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound +](): + pass + + +def it_gets_worse[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, + ItCouldBeGenericOverMultipleTypeVars, +](): + pass + + +def magic[ + Trailing, + Comma, +](): + pass diff --git a/tests/test_black.py b/tests/test_black.py index 00de5b745e7..42b0161d156 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -271,6 +271,15 @@ def test_pep_572_version_detection(self) -> None: versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) + def test_pep_695_version_detection(self) -> None: + for file in ("type_aliases", "type_params"): + source, _ = read_data("py_312", file) + root = black.lib2to3_parse(source) + features = black.get_features_used(root) + self.assertIn(black.Feature.TYPE_PARAMS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY312, versions) + def test_expression_ff(self) -> None: source, expected = read_data("simple_cases", "expression.py") tmp_file = Path(black.dump_to_file(source)) @@ -1533,14 +1542,25 @@ def test_infer_target_version(self) -> None: for version, expected in [ ("3.6", [TargetVersion.PY36]), ("3.11.0rc1", [TargetVersion.PY311]), - (">=3.10", [TargetVersion.PY310, TargetVersion.PY311]), - (">=3.10.6", [TargetVersion.PY310, TargetVersion.PY311]), + (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]), + ( + ">=3.10.6", + [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312], + ), ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]), (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]), - (">3.7,!=3.8,!=3.9", [TargetVersion.PY310, TargetVersion.PY311]), + ( + ">3.7,!=3.8,!=3.9", + [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312], + ), ( "> 3.9.4, != 3.10.3", - [TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311], + [ + TargetVersion.PY39, + TargetVersion.PY310, + TargetVersion.PY311, + TargetVersion.PY312, + ], ), ( "!=3.3,!=3.4", @@ -1552,6 +1572,7 @@ def test_infer_target_version(self) -> None: TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311, + TargetVersion.PY312, ], ), ( @@ -1566,6 +1587,7 @@ def test_infer_target_version(self) -> None: TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311, + TargetVersion.PY312, ], ), ("==3.8.*", [TargetVersion.PY38]), diff --git a/tests/test_format.py b/tests/test_format.py index 5a7b3bb6762..8e0ada99cba 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -134,6 +134,13 @@ def test_python_311(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 11)) +@pytest.mark.parametrize("filename", all_data_cases("py_312")) +def test_python_312(filename: str) -> None: + source, expected = read_data("py_312", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY312}) + assert_format(source, expected, mode, minimum_version=(3, 12)) + + @pytest.mark.parametrize("filename", all_data_cases("fast")) def test_fast_cases(filename: str) -> None: source, expected = read_data("fast", filename) From 898915d5569f503c278ef53cb6f10e003034943c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 10 Jun 2023 19:54:21 +0300 Subject: [PATCH 467/700] Use aware datetimes to represent UTC (#3728) Avoids a Python 3.12 deprecation warning. Subtle difference: previously, timestamps in diff filenames had the `+0000` separated from the timestamp by space. With this, the space is there no more, and there is a colon, as in `+00:00`. --- CHANGES.md | 2 ++ docs/usage_and_configuration/the_basics.md | 4 ++-- src/black/__init__.py | 18 +++++++++--------- src/blackd/__init__.py | 10 +++++----- tests/test_black.py | 10 +++++----- tests/test_blackd.py | 2 +- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fb3dea8c348..658faad3a78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -42,6 +42,8 @@ +- Use aware UTC datetimes internally, avoids deprecation warning on Python 3.12 (#3728) + ### _Blackd_ diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 48619c6bbe8..6f3a3cff30c 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -164,8 +164,8 @@ If you'd like colored diffs, you can enable them with `--color`. ```console $ black test.py --diff ---- test.py 2021-03-08 22:23:40.848954 +0000 -+++ test.py 2021-03-08 22:23:47.126319 +0000 +--- test.py 2021-03-08 22:23:40.848954+00:00 ++++ test.py 2021-03-08 22:23:47.126319+00:00 @@ -1 +1 @@ -print ( 'hello, world' ) +print("hello, world") diff --git a/src/black/__init__.py b/src/black/__init__.py index 8a759aa493a..dbcb559f09d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -7,7 +7,7 @@ import traceback from contextlib import contextmanager from dataclasses import replace -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from json.decoder import JSONDecodeError from pathlib import Path @@ -807,7 +807,7 @@ def format_file_in_place( elif src.suffix == ".ipynb": mode = replace(mode, is_ipynb=True) - then = datetime.utcfromtimestamp(src.stat().st_mtime) + then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc) header = b"" with open(src, "rb") as buf: if mode.skip_source_first_line: @@ -828,9 +828,9 @@ def format_file_in_place( with open(src, "w", encoding=encoding, newline=newline) as f: f.write(dst_contents) elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - now = datetime.utcnow() - src_name = f"{src}\t{then} +0000" - dst_name = f"{src}\t{now} +0000" + now = datetime.now(timezone.utc) + src_name = f"{src}\t{then}" + dst_name = f"{src}\t{now}" if mode.is_ipynb: diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name) else: @@ -868,7 +868,7 @@ def format_stdin_to_stdout( write a diff to stdout. The `mode` argument is passed to :func:`format_file_contents`. """ - then = datetime.utcnow() + then = datetime.now(timezone.utc) if content is None: src, encoding, newline = decode_bytes(sys.stdin.buffer.read()) @@ -893,9 +893,9 @@ def format_stdin_to_stdout( dst += "\n" f.write(dst) elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - now = datetime.utcnow() - src_name = f"STDIN\t{then} +0000" - dst_name = f"STDOUT\t{now} +0000" + now = datetime.now(timezone.utc) + src_name = f"STDIN\t{then}" + dst_name = f"STDOUT\t{now}" d = diff(src, dst, src_name, dst_name) if write_back == WriteBack.COLOR_DIFF: d = color_diff(d) diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index d331ad000bc..c1b69feed63 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -1,7 +1,7 @@ import asyncio import logging from concurrent.futures import Executor, ProcessPoolExecutor -from datetime import datetime +from datetime import datetime, timezone from functools import partial from multiprocessing import freeze_support from typing import Set, Tuple @@ -138,7 +138,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" req_str = req_bytes.decode(charset) - then = datetime.utcnow() + then = datetime.now(timezone.utc) header = "" if skip_source_first_line: @@ -165,9 +165,9 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: # Only output the diff in the HTTP response only_diff = bool(request.headers.get(DIFF_HEADER, False)) if only_diff: - now = datetime.utcnow() - src_name = f"In\t{then} +0000" - dst_name = f"Out\t{now} +0000" + now = datetime.now(timezone.utc) + src_name = f"In\t{then}" + dst_name = f"Out\t{now}" loop = asyncio.get_event_loop() formatted_str = await loop.run_in_executor( executor, diff --git a/tests/test_black.py b/tests/test_black.py index 42b0161d156..5f2e6f5b14c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -207,8 +207,8 @@ def test_piping(self) -> None: def test_piping_diff(self) -> None: diff_header = re.compile( - r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d " - r"\+\d\d\d\d" + r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d" + r"\+\d\d:\d\d" ) source, _ = read_data("simple_cases", "expression.py") expected, _ = read_data("simple_cases", "expression.diff") @@ -300,7 +300,7 @@ def test_expression_diff(self) -> None: tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " - r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d" ) try: result = BlackRunner().invoke( @@ -411,7 +411,7 @@ def test_skip_magic_trailing_comma(self) -> None: tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " - r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d" ) try: result = BlackRunner().invoke( @@ -1750,7 +1750,7 @@ def test_bpo_2142_workaround(self) -> None: tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " - r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d" ) try: result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)]) diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 5b6461f7685..325bd7dd5aa 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -114,7 +114,7 @@ async def test_blackd_pyi(self) -> None: @unittest_run_loop async def test_blackd_diff(self) -> None: diff_header = re.compile( - r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d" ) source, _ = read_data("miscellaneous", "blackd_diff") From c76e0b03ec706619cc76bbd45e9c355c18462e2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:05:49 -0700 Subject: [PATCH 468/700] Bump pypa/cibuildwheel from 2.13.0 to 2.13.1 (#3729) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.13.0 to 2.13.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.13.0...v2.13.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 20787f71cee..06600fcbc45 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.13.0 + uses: pypa/cibuildwheel@v2.13.1 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 688f78d380706fd20776b31eb1ead98ba3f45839 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:08:45 -0700 Subject: [PATCH 469/700] Bump peter-evans/create-or-update-comment from 3.0.1 to 3.0.2 (#3730) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b...c6c9a1a66007646a28c153e2a8580a5bad27bcfa) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 95dd9b01dba..22c293f91d2 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -41,7 +41,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From 35722dff623f3cdf5018e3f1183cd4e02e91caa8 Mon Sep 17 00:00:00 2001 From: Alwyn Kik Date: Mon, 12 Jun 2023 21:20:31 +0200 Subject: [PATCH 470/700] Max line length with bugbear (#3731) * Make phrasing for flake8 users more concise max-line-length should be 80 with flake8-bugbear Fixes #3716 * Re-add rationale and an explanation for disabling E203 * Run pre-commit --- docs/the_black_code_style/current_style.md | 50 +++++++++++----------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index e2625f9e16e..0fb59fe5aae 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -160,33 +160,35 @@ harder to work with line lengths exceeding 100 characters. It also adversely aff side-by-side diff review on typical screen resolutions. Long lines also make it harder to present code neatly in documentation or talk slides. -If you're using Flake8, you can bump `max-line-length` to 88 and mostly forget about it. -However, it's better if you use [Bugbear](https://github.com/PyCQA/flake8-bugbear)'s -B950 warning instead of E501, and bump the max line length to 88 (or the `--line-length` -you used for black), which will align more with black's _"try to respect -`--line-length`, but don't become crazy if you can't"_. You'd do it like this: - -```ini -[flake8] -max-line-length = 88 -... -select = C,E,F,W,B,B950 -extend-ignore = E203, E501 -``` +#### Flake8 -Explanation of why E203 is disabled can be found further in this documentation. And if -you're curious about the reasoning behind B950, -[Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings) -explains it. The tl;dr is "it's like highway speed limits, we won't bother you if you -overdo it by a few km/h". +If you use Flake8, you have a few options: -**If you're looking for a minimal, black-compatible flake8 configuration:** +1. Recommended is using [Bugbear](https://github.com/PyCQA/flake8-bugbear) and enabling + its B950 check instead of using Flake8's E501, because it aligns with Black's 10% + rule. Install Bugbear and use the following config: -```ini -[flake8] -max-line-length = 88 -extend-ignore = E203 -``` + ```ini + [flake8] + max-line-length = 80 + ... + select = C,E,F,W,B,B950 + extend-ignore = E203, E501 + ``` + + The rationale for E950 is explained in + [Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings). + +2. For a minimally compatible config: + + ```ini + [flake8] + max-line-length = 88 + extend-ignore = E203 + ``` + +An explanation of why E203 is disabled can be found in the [Slices section](#slices) of +this page. ### Empty lines From 01b8d3d4095ebdb91d0d39012a517931625c63cb Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Thu, 15 Jun 2023 17:08:26 -0700 Subject: [PATCH 471/700] Do not add trailing commas to return type annotations using PEP 604 unions (#3735) Fix #3638: Do not add trailing commas to return type annotations using PEP 604 unions. --- CHANGES.md | 3 +++ src/black/linegen.py | 7 +++++++ tests/data/simple_cases/pep_604.py | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/data/simple_cases/pep_604.py diff --git a/CHANGES.md b/CHANGES.md index 658faad3a78..fd4d911287d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ +- Fix a bug where an illegal trailing comma was added to return type annotations using + PEP 604 unions (#3735) + ### Preview style diff --git a/src/black/linegen.py b/src/black/linegen.py index 0091cbb3bd1..ad21307c311 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -918,6 +918,13 @@ def bracket_split_build_line( ) if isinstance(node, Node) and isinstance(node.prev_sibling, Leaf) ) + # Except the false negatives above for PEP 604 unions where we + # can't add the comma. + and not ( + leaves[0].parent + and leaves[0].parent.next_sibling + and leaves[0].parent.next_sibling.type == token.VBAR + ) ) if original.is_import or no_commas: diff --git a/tests/data/simple_cases/pep_604.py b/tests/data/simple_cases/pep_604.py new file mode 100644 index 00000000000..b68d59d6440 --- /dev/null +++ b/tests/data/simple_cases/pep_604.py @@ -0,0 +1,25 @@ +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None: + pass + + +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | my_module.EvenMoreType | None: + pass + + +# output + + +def some_very_long_name_function() -> ( + my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None +): + pass + + +def some_very_long_name_function() -> ( + my_module.Asdf + | my_module.AnotherType + | my_module.YetAnotherType + | my_module.EvenMoreType + | None +): + pass From e7783e9ab27e95cd9b50c2ef84174f4e1c9d60cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 06:58:20 -0700 Subject: [PATCH 472/700] Bump myst-parser from 1.0.0 to 2.0.0 in /docs (#3738) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 1.0.0 to 2.0.0. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v1.0.0...v2.0.0) --- updated-dependencies: - dependency-name: myst-parser dependency-type: direct:production update-type: version-update:semver-major ... 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 7b26d089b01..f1b47c69413 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -myst-parser==1.0.0 +myst-parser==2.0.0 Sphinx==6.1.3 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 From d1248ca9beaf0ba526d265f4108836d89cf551b7 Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Tue, 20 Jun 2023 07:06:03 -0700 Subject: [PATCH 473/700] Doc: updating url link (#3739) --- docs/contributing/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing/index.md b/docs/contributing/index.md index f56e57c9e90..3314c8eaa39 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -24,7 +24,8 @@ not very). This is deliberate. _Black_ aims to provide a consistent style and ta opportunities for arguing about style. Bug reports and fixes are always welcome! Please follow the -[issue template on GitHub](https://github.com/psf/black/issues/new) for best results. +[issue templates on GitHub](https://github.com/psf/black/issues/new/choose) for best +results. Before you suggest a new feature or configuration knob, ask yourself why you want it. If it enables better integration with some workflow, fixes an inconsistency, speeds things From 453828d17d50e4bf8bdef972fa6815258e380034 Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Fri, 23 Jun 2023 01:21:49 -0300 Subject: [PATCH 474/700] Fix not honouring pyproject.toml when using stdin and calling black from parent directory (#3719) Co-authored-by: Renan Rodrigues --- CHANGES.md | 2 ++ src/black/__init__.py | 5 ++++- src/black/files.py | 6 ++++-- tests/test_black.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fd4d911287d..e1028c17681 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691) +- Fix black not honouring `pyproject.toml` settings when running `--stdin-filename` and + the `pyproject.toml` found isn't in the current working directory (#3719) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index dbcb559f09d..60a339cdd12 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -127,7 +127,9 @@ def read_pyproject_toml( otherwise. """ if not value: - value = find_pyproject_toml(ctx.params.get("src", ())) + value = find_pyproject_toml( + ctx.params.get("src", ()), ctx.params.get("stdin_filename", None) + ) if value is None: return None @@ -362,6 +364,7 @@ def validate_regex( @click.option( "--stdin-filename", type=str, + is_eager=True, help=( "The name of the file when passing it through stdin. Useful to make " "sure Black will respect --force-exclude option on some " diff --git a/src/black/files.py b/src/black/files.py index 8c0131126b7..65b2d0a8402 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -89,9 +89,11 @@ def find_project_root( return directory, "file system root" -def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: +def find_pyproject_toml( + path_search_start: Tuple[str, ...], stdin_filename: Optional[str] = None +) -> Optional[str]: """Find the absolute filepath to a pyproject.toml if it exists""" - path_project_root, _ = find_project_root(path_search_start) + path_project_root, _ = find_project_root(path_search_start, stdin_filename) path_pyproject_toml = path_project_root / "pyproject.toml" if path_pyproject_toml.is_file(): return str(path_pyproject_toml) diff --git a/tests/test_black.py b/tests/test_black.py index 5f2e6f5b14c..abb304a246d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -104,6 +104,7 @@ class FakeContext(click.Context): def __init__(self) -> None: self.default_map: Dict[str, Any] = {} + self.params: Dict[str, Any] = {} # Dummy root, since most of the tests don't care about it self.obj: Dict[str, Any] = {"root": PROJECT_ROOT} @@ -1620,6 +1621,39 @@ def test_read_pyproject_toml(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + def test_read_pyproject_toml_from_stdin(self) -> None: + with TemporaryDirectory() as workspace: + root = Path(workspace) + + src_dir = root / "src" + src_dir.mkdir() + + src_pyproject = src_dir / "pyproject.toml" + src_pyproject.touch() + + test_toml_file = THIS_DIR / "test.toml" + src_pyproject.write_text(test_toml_file.read_text()) + + src_python = src_dir / "foo.py" + src_python.touch() + + fake_ctx = FakeContext() + fake_ctx.params["src"] = ("-",) + fake_ctx.params["stdin_filename"] = str(src_python) + + with change_directory(root): + black.read_pyproject_toml(fake_ctx, FakeParameter(), None) + + config = fake_ctx.default_map + self.assertEqual(config["verbose"], "1") + self.assertEqual(config["check"], "no") + self.assertEqual(config["diff"], "y") + self.assertEqual(config["color"], "True") + self.assertEqual(config["line_length"], "79") + self.assertEqual(config["target_version"], ["py36", "py37", "py38"]) + self.assertEqual(config["exclude"], r"\.pyi?$") + self.assertEqual(config["include"], r"\.py?$") + @pytest.mark.incompatible_with_mypyc def test_find_project_root(self) -> None: with TemporaryDirectory() as workspace: From c732a1f13a4b75d84dfd74eda0a57a55936d2e22 Mon Sep 17 00:00:00 2001 From: Stian Jensen Date: Fri, 23 Jun 2023 06:22:28 +0200 Subject: [PATCH 475/700] Build with mypyc 1.3 (#3697) Several new versions of mypyc has been released since the last upgrade, and they include some performance improvements which could make the compiled version of Black run faster. https://mypy-lang.org/news.html The latest version of hatch-mypyc allows being installed next the 1.x series of mypy. --- CHANGES.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e1028c17681..460f9c95114 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ +- Upgrade mypyc from 0.991 to 1.3 (#3697) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index 6803a627e9a..d44623fdbd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,8 +119,8 @@ sources = ["src"] [tool.hatch.build.targets.wheel.hooks.mypyc] enable-by-default = false dependencies = [ - "hatch-mypyc>=0.13.0", - "mypy==0.991", + "hatch-mypyc>=0.16.0", + "mypy==1.3", # Required stubs to be removed when the packages support PEP 561 themselves "types-typed-ast>=1.4.2", ] From 7be273531852a16a407fcb66d5efeede0f7ca474 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 24 Jun 2023 16:06:12 -0700 Subject: [PATCH 476/700] Allow specifying `--workers` via environment variable (#3743) --- CHANGES.md | 2 ++ docs/usage_and_configuration/the_basics.md | 16 +++++++++++++++- src/black/__init__.py | 5 ++++- src/black/concurrency.py | 3 ++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 460f9c95114..9717867df4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ +- The `--workers` argument to Black can now be specified via the `BLACK_NUM_WORKERS` + environment variable (#3743) - `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691) - Fix black not honouring `pyproject.toml` settings when running `--stdin-filename` and the `pyproject.toml` found isn't in the current working directory (#3719) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 6f3a3cff30c..2a461487210 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -246,7 +246,8 @@ respect the `--force-exclude` option on some editors that rely on using stdin. #### `-W`, `--workers` When _Black_ formats multiple files, it may use a process pool to speed up formatting. -This option controls the number of parallel workers. +This option controls the number of parallel workers. This can also be specified via the +`BLACK_NUM_WORKERS` environment variable. #### `-q`, `--quiet` @@ -296,6 +297,19 @@ Read configuration options from a configuration file. See Show available command-line options and exit. +### Environment variable options + +_Black_ supports the following configuration via environment variables. + +#### `BLACK_CACHE_DIR` + +The directory where _Black_ should store its cache. + +#### `BLACK_NUM_WORKERS` + +The number of parallel workers _Black_ should use. The command line option `-W` / +`--workers` takes precedence over this environment variable. + ### Code input alternatives _Black_ supports formatting code via stdin, with the result being printed to stdout. diff --git a/src/black/__init__.py b/src/black/__init__.py index 60a339cdd12..3451c86b508 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -376,7 +376,10 @@ def validate_regex( "--workers", type=click.IntRange(min=1), default=None, - help="Number of parallel workers [default: number of CPUs in the system]", + help=( + "Number of parallel workers [default: BLACK_NUM_WORKERS environment variable " + "or number of CPUs in the system]" + ), ) @click.option( "-q", diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 1598f51e43f..893eba6675a 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -80,7 +80,8 @@ def reformat_many( executor: Executor if workers is None: - workers = os.cpu_count() or 1 + workers = int(os.environ.get("BLACK_NUM_WORKERS", 0)) + workers = workers or os.cpu_count() or 1 if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 workers = min(workers, 60) From 93989e995da7fa22c26abf8cc805273940265f5b Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:27:47 -0700 Subject: [PATCH 477/700] Integrate verbose logging with get_sources (#3749) Currently the verbose logging for "Sources to be formatted" is a little suspect in that it is a completely different code path from `get_sources`. This can result in bugs like https://github.com/psf/black/pull/3216#issuecomment-1213557359 and generally limits the value of these logs. This does change the "when" of this log, but the colours help separate it from the even more verbose logs. --- CHANGES.md | 1 + src/black/__init__.py | 34 +++++++++++++--------------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9717867df4e..a9d4d9d63c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,6 +52,7 @@ - Use aware UTC datetimes internally, avoids deprecation warning on Python 3.12 (#3728) +- Change verbose logging to exactly mirror _Black_'s logic for source discovery (#3749) ### _Blackd_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 3451c86b508..222cb3ca03d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -484,26 +484,6 @@ def main( # noqa: C901 fg="blue", ) - normalized = [ - ( - (source, source) - if source == "-" - else (normalize_path_maybe_ignore(Path(source), root), source) - ) - for source in src - ] - srcs_string = ", ".join( - [ - ( - f'"{_norm}"' - if _norm - else f'\033[31m"{source} (skipping - invalid)"\033[34m' - ) - for _norm, source in normalized - ] - ) - out(f"Sources to be formatted: {srcs_string}", fg="blue") - if config: config_source = ctx.get_parameter_source("config") user_level_config = str(find_user_pyproject_toml()) @@ -654,9 +634,15 @@ def get_sources( is_stdin = False if is_stdin or p.is_file(): - normalized_path = normalize_path_maybe_ignore(p, ctx.obj["root"], report) + normalized_path: Optional[str] = normalize_path_maybe_ignore( + p, ctx.obj["root"], report + ) if normalized_path is None: + if verbose: + out(f'Skipping invalid source: "{normalized_path}"', fg="red") continue + if verbose: + out(f'Found input source: "{normalized_path}"', fg="blue") normalized_path = "/" + normalized_path # Hard-exclude any files that matches the `--force-exclude` regex. @@ -679,6 +665,9 @@ def get_sources( sources.add(p) elif p.is_dir(): p = root / normalize_path_maybe_ignore(p, ctx.obj["root"], report) + if verbose: + out(f'Found input source directory: "{p}"', fg="blue") + if using_default_exclude: gitignore = { root: root_gitignore, @@ -699,9 +688,12 @@ def get_sources( ) ) elif s == "-": + if verbose: + out("Found input source stdin", fg="blue") sources.add(p) else: err(f"invalid path: {s}") + return sources From e1036119f264a846bb049fad8404df318bc2f455 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 25 Jun 2023 06:53:26 -0700 Subject: [PATCH 478/700] Check self format for the whole repo (#3750) `black .` is changing things in gallery and scripts for me --- .github/workflows/test.yml | 2 +- gallery/gallery.py | 6 ++---- scripts/make_width_table.py | 6 ++---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ca2a469147..608c58af2ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,4 +103,4 @@ jobs: python -m pip install -e ".[uvloop]" - name: Format ourselves - run: python -m black --check src/ + run: python -m black --check . diff --git a/gallery/gallery.py b/gallery/gallery.py index 38e52e34795..ba5d6f65fbe 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -243,11 +243,9 @@ def format_repos(repos: Tuple[Path, ...], options: Namespace) -> None: def main() -> None: - parser = ArgumentParser( - description="""Black Gallery is a script that + parser = ArgumentParser(description="""Black Gallery is a script that automates the process of applying different Black versions to a selected - PyPI package and seeing the results between versions.""" - ) + PyPI package and seeing the results between versions.""") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("-p", "--pypi-package", help="PyPI package to download.") diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 09aca9c34b5..89c202553d3 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -49,8 +49,7 @@ def make_width_table() -> Iterable[Tuple[int, int, int]]: def main() -> None: table_path = join(dirname(__file__), "..", "src", "black", "_width_table.py") with open(table_path, "w") as f: - f.write( - f"""# Generated by {basename(__file__)} + f.write(f"""# Generated by {basename(__file__)} # wcwidth {wcwidth.__version__} # Unicode {wcwidth.list_versions()[-1]} import sys @@ -62,8 +61,7 @@ def main() -> None: from typing import Final WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [ -""" - ) +""") for triple in make_width_table(): f.write(f" {triple!r},\n") f.write("]\n") From 31b3b6701d2cfae072900f9d45dc8f1737ab282b Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:47:55 -0700 Subject: [PATCH 479/700] Decrease cost of ipynb code path when unneeded (#3748) IPython is a very expensive import, like, at least 300ms. I'd also venture that it's much more common than tokenize-rt, which is like 30ms. I work in a repo where I use black, have IPython installed and there happen to be a couple notebooks (that we don't want formatted). I know I can force exclude ipynb, but this change doesn't really have a cost. --- CHANGES.md | 2 ++ src/black/handle_ipynb_magics.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a9d4d9d63c9..6fa0e4b7cc0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,8 @@ +- Avoid importing `IPython` in a case where we wouldn't need it (#3748) + ### Output diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 9e1af757c32..2b6b9209211 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -58,8 +58,13 @@ class Replacement: @lru_cache() def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: try: - import IPython # noqa:F401 + # isort: off + # tokenize_rt is less commonly installed than IPython + # and IPython is expensive to import import tokenize_rt # noqa:F401 + import IPython # noqa:F401 + + # isort: on except ModuleNotFoundError: if verbose or not quiet: msg = ( From 63481bb9264a8c3756577089414d222da9c7fab0 Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Tue, 27 Jun 2023 07:23:39 -0700 Subject: [PATCH 480/700] Fix a magical comment caused internal error (#3740) `is_type_comment` now specifically deals with general type comments for a leaf. `is_type_ignore_comment` now handles type comments contains ignore annotation for a leaf `is_type_ignore_comment_string` used to determine if a string is an ignore type comment --- CHANGES.md | 2 + src/black/linegen.py | 10 ++++- src/black/lines.py | 5 ++- src/black/nodes.py | 23 +++++++++-- ...ine_consecutive_open_parentheses_ignore.py | 41 +++++++++++++++++++ 5 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py diff --git a/CHANGES.md b/CHANGES.md index 6fa0e4b7cc0..2dfed8a0dc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ - Fix a bug where an illegal trailing comma was added to return type annotations using PEP 604 unions (#3735) +- Fix a bug where multi-line open parenthesis magic comment like `type: ignore` were not + correctly parsed (#3740) ### Preview style diff --git a/src/black/linegen.py b/src/black/linegen.py index ad21307c311..5ef3bbd1705 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -49,6 +49,7 @@ is_stub_body, is_stub_suite, is_tuple_containing_walrus, + is_type_ignore_comment_string, is_vararg, is_walrus_assignment, is_yield, @@ -1399,8 +1400,13 @@ def maybe_make_parens_invisible_in_atom( if is_lpar_token(first) and is_rpar_token(last): middle = node.children[1] # make parentheses invisible - first.value = "" - last.value = "" + if ( + # If the prefix of `middle` includes a type comment with + # ignore annotation, then we do not remove the parentheses + not is_type_ignore_comment_string(middle.prefix.strip()) + ): + first.value = "" + last.value = "" maybe_make_parens_invisible_in_atom( middle, parent=parent, diff --git a/src/black/lines.py b/src/black/lines.py index daf0444d24e..ea8fe520756 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -28,6 +28,7 @@ is_multiline_string, is_one_sequence_between, is_type_comment, + is_type_ignore_comment, is_with_or_async_with_stmt, replace_child, syms, @@ -251,7 +252,7 @@ def contains_uncollapsable_type_comments(self) -> bool: for comment in comments: if is_type_comment(comment): if comment_seen or ( - not is_type_comment(comment, " ignore") + not is_type_ignore_comment(comment) and leaf_id not in ignored_ids ): return True @@ -288,7 +289,7 @@ def contains_unsplittable_type_ignore(self) -> bool: # line. for node in self.leaves[-2:]: for comment in self.comments.get(id(node), []): - if is_type_comment(comment, " ignore"): + if is_type_ignore_comment(comment): return True return False diff --git a/src/black/nodes.py b/src/black/nodes.py index 45070909df4..b019b0c6440 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -816,12 +816,27 @@ def is_async_stmt_or_funcdef(leaf: Leaf) -> bool: ) -def is_type_comment(leaf: Leaf, suffix: str = "") -> bool: - """Return True if the given leaf is a special comment. - Only returns true for type comments for now.""" +def is_type_comment(leaf: Leaf) -> bool: + """Return True if the given leaf is a type comment. This function should only + be used for general type comments (excluding ignore annotations, which should + use `is_type_ignore_comment`). Note that general type comments are no longer + used in modern version of Python, this function may be deprecated in the future.""" t = leaf.type v = leaf.value - return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix) + return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:") + + +def is_type_ignore_comment(leaf: Leaf) -> bool: + """Return True if the given leaf is a type comment with ignore annotation.""" + t = leaf.type + v = leaf.value + return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_ignore_comment_string(v) + + +def is_type_ignore_comment_string(value: str) -> bool: + """Return True if the given string match with type comment with + ignore annotation.""" + return value.startswith("# type: ignore") def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: diff --git a/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py b/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py new file mode 100644 index 00000000000..6ec8bb45408 --- /dev/null +++ b/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py @@ -0,0 +1,41 @@ +# This is a regression test. Issue #3737 + +a = ( # type: ignore + int( # type: ignore + int( # type: ignore + int( # type: ignore + 6 + ) + ) + ) +) + +b = ( + int( + 6 + ) +) + +print( "111") # type: ignore +print( "111" ) # type: ignore +print( "111" ) # type: ignore + + +# output + + +# This is a regression test. Issue #3737 + +a = ( # type: ignore + int( # type: ignore + int( # type: ignore + int(6) # type: ignore + ) + ) +) + +b = int(6) + +print("111") # type: ignore +print("111") # type: ignore +print("111") # type: ignore \ No newline at end of file From f01aaa63a0ff792b932205cbb539f70aca769d7d Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:45:56 -0700 Subject: [PATCH 481/700] Doc: Developer reference update (#3755) --- CHANGES.md | 3 + .../reference/reference_classes.rst | 161 +++++++++++++++++- .../reference/reference_exceptions.rst | 10 +- .../reference/reference_summary.rst | 7 +- src/black/handle_ipynb_magics.py | 3 +- src/black/trans.py | 95 ++++++----- 6 files changed, 220 insertions(+), 59 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2dfed8a0dc4..ba7947a3c7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -76,6 +76,9 @@ +- Updated the _classes_ and _exceptions_ documentation in Developer reference to match + the latest ccode base. (#3755) + ## 23.3.0 ### Highlights diff --git a/docs/contributing/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst index 3931e0e0072..29b25003af2 100644 --- a/docs/contributing/reference/reference_classes.rst +++ b/docs/contributing/reference/reference_classes.rst @@ -3,6 +3,9 @@ *Contents are subject to change.* +Black Classes +~~~~~~~~~~~~~~ + .. currentmodule:: black :class:`BracketTracker` @@ -18,6 +21,12 @@ :members: :special-members: __str__, __bool__ +:class:`RHSResult` +------------------------- + +.. autoclass:: black.lines.RHSResult + :members: + :class:`LinesBlock` ------------------------- @@ -43,6 +52,12 @@ .. autoclass:: black.comments.ProtoComment :members: +:class:`Mode` +--------------------- + +.. autoclass:: black.mode.Mode + :members: + :class:`Report` --------------- @@ -50,6 +65,20 @@ :members: :special-members: __str__ +:class:`Ok` +--------------- + +.. autoclass:: black.rusty.Ok + :show-inheritance: + :members: + +:class:`Err` +--------------- + +.. autoclass:: black.rusty.Err + :show-inheritance: + :members: + :class:`Visitor` ---------------- @@ -57,20 +86,115 @@ :show-inheritance: :members: -Enums -===== +:class:`StringTransformer` +---------------------------- -:class:`Changed` ----------------- +.. autoclass:: black.trans.StringTransformer + :show-inheritance: + :members: -.. autoclass:: black.Changed +:class:`CustomSplit` +---------------------------- + +.. autoclass:: black.trans.CustomSplit + :members: + +:class:`CustomSplitMapMixin` +----------------------------- + +.. autoclass:: black.trans.CustomSplitMapMixin :show-inheritance: :members: -:class:`Mode` ------------------ +:class:`StringMerger` +---------------------- -.. autoclass:: black.Mode +.. autoclass:: black.trans.StringMerger + :show-inheritance: + :members: + +:class:`StringParenStripper` +----------------------------- + +.. autoclass:: black.trans.StringParenStripper + :show-inheritance: + :members: + +:class:`BaseStringSplitter` +----------------------------- + +.. autoclass:: black.trans.BaseStringSplitter + :show-inheritance: + :members: + +:class:`StringSplitter` +----------------------------- + +.. autoclass:: black.trans.StringSplitter + :show-inheritance: + :members: + +:class:`StringParenWrapper` +----------------------------- + +.. autoclass:: black.trans.StringParenWrapper + :show-inheritance: + :members: + +:class:`StringParser` +----------------------------- + +.. autoclass:: black.trans.StringParser + :members: + +:class:`DebugVisitor` +------------------------ + +.. autoclass:: black.debug.DebugVisitor + :show-inheritance: + :members: + +:class:`Replacement` +------------------------ + +.. autoclass:: black.handle_ipynb_magics.Replacement + :members: + +:class:`CellMagic` +------------------------ + +.. autoclass:: black.handle_ipynb_magics.CellMagic + :members: + +:class:`CellMagicFinder` +------------------------ + +.. autoclass:: black.handle_ipynb_magics.CellMagicFinder + :show-inheritance: + :members: + +:class:`OffsetAndMagic` +------------------------ + +.. autoclass:: black.handle_ipynb_magics.OffsetAndMagic + :members: + +:class:`MagicFinder` +------------------------ + +.. autoclass:: black.handle_ipynb_magics.MagicFinder + :show-inheritance: + :members: + +Enum Classes +~~~~~~~~~~~~~ + +Classes inherited from Python `Enum `_ class. + +:class:`Changed` +---------------- + +.. autoclass:: black.report.Changed :show-inheritance: :members: @@ -80,3 +204,24 @@ Enums .. autoclass:: black.WriteBack :show-inheritance: :members: + +:class:`TargetVersion` +---------------------- + +.. autoclass:: black.mode.TargetVersion + :show-inheritance: + :members: + +:class:`Feature` +------------------ + +.. autoclass:: black.mode.Feature + :show-inheritance: + :members: + +:class:`Preview` +------------------ + +.. autoclass:: black.mode.Preview + :show-inheritance: + :members: diff --git a/docs/contributing/reference/reference_exceptions.rst b/docs/contributing/reference/reference_exceptions.rst index aafe61e5017..ab46ebdb628 100644 --- a/docs/contributing/reference/reference_exceptions.rst +++ b/docs/contributing/reference/reference_exceptions.rst @@ -5,8 +5,14 @@ .. currentmodule:: black +.. autoexception:: black.trans.CannotTransform + .. autoexception:: black.linegen.CannotSplit -.. autoexception:: black.NothingChanged +.. autoexception:: black.brackets.BracketMatchError + +.. autoexception:: black.report.NothingChanged + +.. autoexception:: black.parsing.InvalidInput -.. autoexception:: black.InvalidInput +.. autoexception:: black.mode.Deprecated diff --git a/docs/contributing/reference/reference_summary.rst b/docs/contributing/reference/reference_summary.rst index f6ff4681557..c6163d897b6 100644 --- a/docs/contributing/reference/reference_summary.rst +++ b/docs/contributing/reference/reference_summary.rst @@ -3,8 +3,11 @@ Developer reference .. note:: - The documentation here is quite outdated and has been neglected. Many objects worthy - of inclusion aren't documented. Contributions are appreciated! + As of June 2023, the documentation of *Black classes* and *Black exceptions* + has been updated to the latest available version. + + The documentation of *Black functions* is quite outdated and has been neglected. Many + functions worthy of inclusion aren't documented. Contributions are appreciated! *Contents are subject to change.* diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 2b6b9209211..4324819beaf 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -335,7 +335,8 @@ class CellMagicFinder(ast.NodeVisitor): For example, - %%time\nfoo() + %%time\n + foo() would have been transformed to diff --git a/src/black/trans.py b/src/black/trans.py index 1e28ed0656e..4d40cb4bdf6 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -205,11 +205,11 @@ def do_match(self, line: Line) -> TMatchResult: """ Returns: * Ok(string_indices) such that for each index, `line.leaves[index]` - is our target string if a match was able to be made. For - transformers that don't result in more lines (e.g. StringMerger, - StringParenStripper), multiple matches and transforms are done at - once to reduce the complexity. - OR + is our target string if a match was able to be made. For + transformers that don't result in more lines (e.g. StringMerger, + StringParenStripper), multiple matches and transforms are done at + once to reduce the complexity. + OR * Err(CannotTransform), if no match could be made. """ @@ -220,12 +220,12 @@ def do_transform( """ Yields: * Ok(new_line) where new_line is the new transformed line. - OR + OR * Err(CannotTransform) if the transformation failed for some reason. The - `do_match(...)` template method should usually be used to reject - the form of the given Line, but in some cases it is difficult to - know whether or not a Line meets the StringTransformer's - requirements until the transformation is already midway. + `do_match(...)` template method should usually be used to reject + the form of the given Line, but in some cases it is difficult to + know whether or not a Line meets the StringTransformer's + requirements until the transformation is already midway. Side Effects: This method should NOT mutate @line directly, but it MAY mutate the @@ -335,8 +335,8 @@ def pop_custom_splits(self, string: str) -> List[CustomSplit]: Returns: * A list of the custom splits that are mapped to @string, if any - exist. - OR + exist. + OR * [], otherwise. Side Effects: @@ -365,14 +365,14 @@ class StringMerger(StringTransformer, CustomSplitMapMixin): Requirements: (A) The line contains adjacent strings such that ALL of the validation checks listed in StringMerger._validate_msg(...)'s docstring pass. - OR + OR (B) The line contains a string which uses line continuation backslashes. Transformations: Depending on which of the two requirements above where met, either: (A) The string group associated with the target string is merged. - OR + OR (B) All line-continuation backslashes are removed from the target string. Collaborations: @@ -965,17 +965,20 @@ class BaseStringSplitter(StringTransformer): Requirements: * The target string value is responsible for the line going over the - line length limit. It follows that after all of black's other line - split methods have been exhausted, this line (or one of the resulting - lines after all line splits are performed) would still be over the - line_length limit unless we split this string. - AND + line length limit. It follows that after all of black's other line + split methods have been exhausted, this line (or one of the resulting + lines after all line splits are performed) would still be over the + line_length limit unless we split this string. + AND + * The target string is NOT a "pointless" string (i.e. a string that has - no parent or siblings). - AND + no parent or siblings). + AND + * The target string is not followed by an inline comment that appears - to be a pragma. - AND + to be a pragma. + AND + * The target string is not a multiline (i.e. triple-quote) string. """ @@ -1027,7 +1030,7 @@ def _validate(self, line: Line, string_idx: int) -> TResult[None]: Returns: * Ok(None), if ALL of the requirements are met. - OR + OR * Err(CannotTransform), if ANY of the requirements are NOT met. """ LL = line.leaves @@ -1299,9 +1302,9 @@ class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): Requirements: * The line consists ONLY of a single string (possibly prefixed by a - string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE - a trailing comma. - AND + string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE + a trailing comma. + AND * All of the requirements listed in BaseStringSplitter's docstring. Transformations: @@ -1808,26 +1811,26 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): addition to the requirements listed below: * The line is a return/yield statement, which returns/yields a string. - OR + OR * The line is part of a ternary expression (e.g. `x = y if cond else - z`) such that the line starts with `else `, where is - some string. - OR + z`) such that the line starts with `else `, where is + some string. + OR * The line is an assert statement, which ends with a string. - OR + OR * The line is an assignment statement (e.g. `x = ` or `x += - `) such that the variable is being assigned the value of some - string. - OR + `) such that the variable is being assigned the value of some + string. + OR * The line is a dictionary key assignment where some valid key is being - assigned the value of some string. - OR + assigned the value of some string. + OR * The line is an lambda expression and the value is a string. - OR + OR * The line starts with an "atom" string that prefers to be wrapped in - parens. It's preferred to be wrapped when it's is an immediate child of - a list/set/tuple literal, AND the string is surrounded by commas (or is - the first/last child). + parens. It's preferred to be wrapped when it's is an immediate child of + a list/set/tuple literal, AND the string is surrounded by commas (or is + the first/last child). Transformations: The chosen string is wrapped in parentheses and then split at the LPAR. @@ -2273,7 +2276,7 @@ def parse(self, leaves: List[Leaf], string_idx: int) -> int: Returns: The index directly after the last leaf which is apart of the string trailer, if a "trailer" exists. - OR + OR @string_idx + 1, if no string "trailer" exists. """ assert leaves[string_idx].type == token.STRING @@ -2287,11 +2290,11 @@ def _next_state(self, leaf: Leaf) -> bool: """ Pre-conditions: * On the first call to this function, @leaf MUST be the leaf that - was directly after the string leaf in question (e.g. if our target - string is `line.leaves[i]` then the first call to this method must - be `line.leaves[i + 1]`). + was directly after the string leaf in question (e.g. if our target + string is `line.leaves[i]` then the first call to this method must + be `line.leaves[i + 1]`). * On the next call to this function, the leaf parameter passed in - MUST be the leaf directly following @leaf. + MUST be the leaf directly following @leaf. Returns: True iff @leaf is apart of the string's trailer. From 839ef35dc1d72bb6eceac9fa809f095e2edcd12b Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Fri, 30 Jun 2023 07:07:42 -0700 Subject: [PATCH 482/700] CI Test: Deprecating 'set-output' command (#3757) --- CHANGES.md | 2 ++ scripts/diff_shades_gha_helper.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ba7947a3c7d..d7928a30c5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,6 +70,8 @@ - Update GitHub Action to display black output in the job summary (#3688) +- Deprecated `set-output` command in CI test to keep up to date with GitHub's + deprecation announcement (#3757) ### Documentation diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index b5fea5a817d..994fbe05045 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -52,7 +52,13 @@ def set_output(name: str, value: str) -> None: print(f"[INFO]: setting '{name}' to '{value}'") else: print(f"[INFO]: setting '{name}' to [{len(value)} chars]") - print(f"::set-output name={name}::{value}") + + # Originally the `set-output` workflow command was used here, now replaced + # by setting variables through the `GITHUB_OUTPUT` environment variable + # to stay up to date with GitHub's update. + if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + print(f"{name}={value}", file=f) def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any: From 8e618f386995fa89434834e6a793a1057e58112a Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 4 Jul 2023 16:38:39 -0700 Subject: [PATCH 483/700] Enable `PYTHONWARNDEFAULTENCODING = 1` in CI (#3763) --- CHANGES.md | 3 ++ tests/test_black.py | 87 +++++++++++++++++------------------------- tests/test_ipynb.py | 9 ++--- tests/test_no_ipynb.py | 3 +- tox.ini | 4 +- 5 files changed, 44 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d7928a30c5c..acb5a822674 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,9 @@ +- Black is now tested with + [`PYTHONWARNDEFAULTENCODING = 1`](https://docs.python.org/3/library/io.html#io-encoding-warning) + (#3763) - Update GitHub Action to display black output in the job summary (#3688) - Deprecated `set-output` command in CI test to keep up to date with GitHub's deprecation announcement (#3757) diff --git a/tests/test_black.py b/tests/test_black.py index abb304a246d..dee6ead50d0 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -149,8 +149,7 @@ def test_empty_ff(self) -> None: tmp_file = Path(black.dump_to_file()) try: self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES)) - with open(tmp_file, encoding="utf8") as f: - actual = f.read() + actual = tmp_file.read_text(encoding="utf-8") finally: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) @@ -178,7 +177,7 @@ def test_one_empty_line_ff(self) -> None: ff(tmp_file, mode=mode, write_back=black.WriteBack.YES) ) with open(tmp_file, "rb") as f: - actual = f.read().decode("utf8") + actual = f.read().decode("utf-8") finally: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) @@ -198,7 +197,7 @@ def test_piping(self) -> None: f"--line-length={black.DEFAULT_LINE_LENGTH}", f"--config={EMPTY_CONFIG}", ], - input=BytesIO(source.encode("utf8")), + input=BytesIO(source.encode("utf-8")), ) self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) @@ -221,7 +220,7 @@ def test_piping_diff(self) -> None: f"--config={EMPTY_CONFIG}", ] result = BlackRunner().invoke( - black.main, args, input=BytesIO(source.encode("utf8")) + black.main, args, input=BytesIO(source.encode("utf-8")) ) self.assertEqual(result.exit_code, 0) actual = diff_header.sub(DETERMINISTIC_HEADER, result.output) @@ -239,7 +238,7 @@ def test_piping_diff_with_color(self) -> None: f"--config={EMPTY_CONFIG}", ] result = BlackRunner().invoke( - black.main, args, input=BytesIO(source.encode("utf8")) + black.main, args, input=BytesIO(source.encode("utf-8")) ) actual = result.output # Again, the contents are checked in a different test, so only look for colors. @@ -286,8 +285,7 @@ def test_expression_ff(self) -> None: tmp_file = Path(black.dump_to_file(source)) try: self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES)) - with open(tmp_file, encoding="utf8") as f: - actual = f.read() + actual = tmp_file.read_text(encoding="utf-8") finally: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) @@ -390,8 +388,7 @@ def test_skip_source_first_line(self) -> None: black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"] ) self.assertEqual(result.exit_code, 0) - with open(tmp_file, encoding="utf8") as f: - actual = f.read() + actual = tmp_file.read_text(encoding="utf-8") self.assertFormatEqual(source, actual) def test_skip_source_first_line_when_mixing_newlines(self) -> None: @@ -1081,7 +1078,7 @@ def test_works_in_mono_process_only_environment(self) -> None: (workspace / "one.py").resolve(), (workspace / "two.py").resolve(), ]: - f.write_text('print("hello")\n') + f.write_text('print("hello")\n', encoding="utf-8") self.invokeBlack([str(workspace)]) @event_loop() @@ -1118,11 +1115,9 @@ def test_single_file_force_pyi(self) -> None: contents, expected = read_data("miscellaneous", "force_pyi") with cache_dir() as workspace: path = (workspace / "file.py").resolve() - with open(path, "w") as fh: - fh.write(contents) + path.write_text(contents, encoding="utf-8") self.invokeBlack([str(path), "--pyi"]) - with open(path, "r") as fh: - actual = fh.read() + actual = path.read_text(encoding="utf-8") # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) self.assertIn(str(path), pyi_cache) @@ -1143,12 +1138,10 @@ def test_multi_file_force_pyi(self) -> None: (workspace / "file2.py").resolve(), ] for path in paths: - with open(path, "w") as fh: - fh.write(contents) + path.write_text(contents, encoding="utf-8") self.invokeBlack([str(p) for p in paths] + ["--pyi"]) for path in paths: - with open(path, "r") as fh: - actual = fh.read() + actual = path.read_text(encoding="utf-8") self.assertEqual(actual, expected) # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) @@ -1160,7 +1153,7 @@ def test_multi_file_force_pyi(self) -> None: def test_pipe_force_pyi(self) -> None: source, expected = read_data("miscellaneous", "force_pyi") result = CliRunner().invoke( - black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8")) + black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8")) ) self.assertEqual(result.exit_code, 0) actual = result.output @@ -1172,11 +1165,9 @@ def test_single_file_force_py36(self) -> None: source, expected = read_data("miscellaneous", "force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() - with open(path, "w") as fh: - fh.write(source) + path.write_text(source, encoding="utf-8") self.invokeBlack([str(path), *PY36_ARGS]) - with open(path, "r") as fh: - actual = fh.read() + actual = path.read_text(encoding="utf-8") # verify cache with --target-version is separate py36_cache = black.read_cache(py36_mode) self.assertIn(str(path), py36_cache) @@ -1195,12 +1186,10 @@ def test_multi_file_force_py36(self) -> None: (workspace / "file2.py").resolve(), ] for path in paths: - with open(path, "w") as fh: - fh.write(source) + path.write_text(source, encoding="utf-8") self.invokeBlack([str(p) for p in paths] + PY36_ARGS) for path in paths: - with open(path, "r") as fh: - actual = fh.read() + actual = path.read_text(encoding="utf-8") self.assertEqual(actual, expected) # verify cache with --target-version is separate pyi_cache = black.read_cache(py36_mode) @@ -1214,7 +1203,7 @@ def test_pipe_force_py36(self) -> None: result = CliRunner().invoke( black.main, ["-", "-q", "--target-version=py36"], - input=BytesIO(source.encode("utf8")), + input=BytesIO(source.encode("utf-8")), ) self.assertEqual(result.exit_code, 0) actual = result.output @@ -1443,11 +1432,11 @@ def test_preserves_line_endings_via_stdin(self) -> None: contents = nl.join(["def f( ):", " pass"]) runner = BlackRunner() result = runner.invoke( - black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8")) + black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8")) ) self.assertEqual(result.exit_code, 0) output = result.stdout_bytes - self.assertIn(nl.encode("utf8"), output) + self.assertIn(nl.encode("utf-8"), output) if nl == "\n": self.assertNotIn(b"\r\n", output) @@ -1631,8 +1620,8 @@ def test_read_pyproject_toml_from_stdin(self) -> None: src_pyproject = src_dir / "pyproject.toml" src_pyproject.touch() - test_toml_file = THIS_DIR / "test.toml" - src_pyproject.write_text(test_toml_file.read_text()) + test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8") + src_pyproject.write_text(test_toml_content, encoding="utf-8") src_python = src_dir / "foo.py" src_python.touch() @@ -1985,10 +1974,10 @@ def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: cache_file = get_cache_file(mode) - cache_file.write_text("this is not a pickle") + cache_file.write_text("this is not a pickle", encoding="utf-8") assert black.read_cache(mode) == {} src = (workspace / "test.py").resolve() - src.write_text("print('hello')") + src.write_text("print('hello')", encoding="utf-8") invokeBlack([str(src)]) cache = black.read_cache(mode) assert str(src) in cache @@ -1997,10 +1986,10 @@ def test_cache_single_file_already_cached(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() - src.write_text("print('hello')") + src.write_text("print('hello')", encoding="utf-8") black.write_cache({}, [src], mode) invokeBlack([str(src)]) - assert src.read_text() == "print('hello')" + assert src.read_text(encoding="utf-8") == "print('hello')" @event_loop() def test_cache_multiple_files(self) -> None: @@ -2009,17 +1998,13 @@ def test_cache_multiple_files(self) -> None: "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor ): one = (workspace / "one.py").resolve() - with one.open("w") as fobj: - fobj.write("print('hello')") + one.write_text("print('hello')", encoding="utf-8") two = (workspace / "two.py").resolve() - with two.open("w") as fobj: - fobj.write("print('hello')") + two.write_text("print('hello')", encoding="utf-8") black.write_cache({}, [one], mode) invokeBlack([str(workspace)]) - with one.open("r") as fobj: - assert fobj.read() == "print('hello')" - with two.open("r") as fobj: - assert fobj.read() == 'print("hello")\n' + assert one.read_text(encoding="utf-8") == "print('hello')" + assert two.read_text(encoding="utf-8") == 'print("hello")\n' cache = black.read_cache(mode) assert str(one) in cache assert str(two) in cache @@ -2029,8 +2014,7 @@ def test_no_cache_when_writeback_diff(self, color: bool) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") + src.write_text("print('hello')", encoding="utf-8") with patch("black.read_cache") as read_cache, patch( "black.write_cache" ) as write_cache: @@ -2049,8 +2033,7 @@ def test_output_locking_when_writeback_diff(self, color: bool) -> None: with cache_dir() as workspace: for tag in range(0, 4): src = (workspace / f"test{tag}.py").resolve() - with src.open("w") as fobj: - fobj.write("print('hello')") + src.write_text("print('hello')", encoding="utf-8") with patch( "black.concurrency.Manager", wraps=multiprocessing.Manager ) as mgr: @@ -2120,11 +2103,9 @@ def test_failed_formatting_does_not_get_cached(self) -> None: "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor ): failing = (workspace / "failing.py").resolve() - with failing.open("w") as fobj: - fobj.write("not actually python") + failing.write_text("not actually python", encoding="utf-8") clean = (workspace / "clean.py").resolve() - with clean.open("w") as fobj: - fobj.write('print("hello")\n') + clean.write_text('print("hello")\n', encoding="utf-8") invokeBlack([str(workspace)], exit_code=123) cache = black.read_cache(mode) assert str(failing) not in cache diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 7aa2e91dd00..91e7901125b 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -439,8 +439,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_single( jupyter_dependencies_are_installed.cache_clear() nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" - with open(nb) as src, open(tmp_nb, "w") as dst: - dst.write(src.read()) + tmp_nb.write_bytes(nb.read_bytes()) monkeypatch.setattr( "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) @@ -465,8 +464,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( jupyter_dependencies_are_installed.cache_clear() nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" - with open(nb) as src, open(tmp_nb, "w") as dst: - dst.write(src.read()) + tmp_nb.write_bytes(nb.read_bytes()) monkeypatch.setattr( "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False ) @@ -483,8 +481,7 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( def test_ipynb_flag(tmp_path: pathlib.Path) -> None: nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb" - with open(nb) as src, open(tmp_nb, "w") as dst: - dst.write(src.read()) + tmp_nb.write_bytes(nb.read_bytes()) result = runner.invoke( main, [ diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py index b63ecde8896..12c820def39 100644 --- a/tests/test_no_ipynb.py +++ b/tests/test_no_ipynb.py @@ -27,8 +27,7 @@ def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None: runner = CliRunner() nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" - with open(nb) as src, open(tmp_nb, "w") as dst: - dst.write(src.read()) + tmp_nb.write_bytes(nb.read_bytes()) result = runner.invoke(main, [str(tmp_path)]) expected_output = ( "Skipping .ipynb files as Jupyter dependencies are not installed.\n" diff --git a/tox.ini b/tox.ini index 4934514264b..f8e1a785331 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,9 @@ isolated_build = true envlist = {,ci-}py{37,38,39,310,311,py3},fuzz,run_self [testenv] -setenv = PYTHONPATH = {toxinidir}/src +setenv = + PYTHONPATH = {toxinidir}/src + PYTHONWARNDEFAULTENCODING = 1 skip_install = True # We use `recreate=True` because otherwise, on the second run of `tox -e py`, # the `no_jupyter` tests would run with the jupyter extra dependencies installed. From cf4cc2981900565ab931aada176abf08a1f5782d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 4 Jul 2023 22:45:57 -0700 Subject: [PATCH 484/700] Better error message for invalid exclude types (#3764) --- CHANGES.md | 2 ++ src/black/__init__.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index acb5a822674..bfa021617cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,8 @@ - `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691) - Fix black not honouring `pyproject.toml` settings when running `--stdin-filename` and the `pyproject.toml` found isn't in the current working directory (#3719) +- Black will now error if `exclude` and `extend-exclude` have invalid data types in + `pyproject.toml`, instead of silently doing the wrong thing (#3764) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 222cb3ca03d..b6611bef84b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -157,6 +157,16 @@ def read_pyproject_toml( "target-version", "Config key target-version must be a list" ) + exclude = config.get("exclude") + if exclude is not None and not isinstance(exclude, str): + raise click.BadOptionUsage("exclude", "Config key exclude must be a string") + + extend_exclude = config.get("extend_exclude") + if extend_exclude is not None and not isinstance(extend_exclude, str): + raise click.BadOptionUsage( + "extend-exclude", "Config key extend-exclude must be a string" + ) + default_map: Dict[str, Any] = {} if ctx.default_map: default_map.update(ctx.default_map) From b4dca26c7d93f930bbd5a7b552807370b60d4298 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 5 Jul 2023 10:08:04 -0700 Subject: [PATCH 485/700] Drop support for Python 3.7 (#3765) --- .github/workflows/fuzz.yml | 2 +- .github/workflows/test.yml | 2 +- CHANGES.md | 3 + docs/integrations/editors.md | 54 ---------------- mypy.ini | 2 +- pyproject.toml | 10 +-- scripts/make_width_table.py | 8 +-- src/black/_width_table.py | 8 +-- src/black/brackets.py | 8 +-- src/black/comments.py | 8 +-- src/black/mode.py | 8 +-- src/black/nodes.py | 6 +- src/black/parsing.py | 109 +++++++-------------------------- src/black/strings.py | 10 +-- src/black/trans.py | 8 +-- src/blib2to3/pgen2/token.py | 5 +- src/blib2to3/pgen2/tokenize.py | 5 +- 17 files changed, 43 insertions(+), 213 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 373e1500ee9..4439148a1c7 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 608c58af2ee..92d7d411510 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.7", "pypy-3.8"] + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index bfa021617cb..24ca54a82ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ +- Runtime support for Python 3.7 has been removed. Formatting 3.7 code will still be + supported until further notice (#3765) + ### Stable style diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 74c6a283ab8..ff563068e79 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -334,60 +334,6 @@ To run _Black_ on a key press (e.g. F9 below), add this: nnoremap :Black ``` -#### Troubleshooting - -**How to get Vim with Python 3.6?** On Ubuntu 17.10 Vim comes with Python 3.6 by -default. On macOS with Homebrew run: `brew install vim`. When building Vim from source, -use: `./configure --enable-python3interp=yes`. There's many guides online how to do -this. - -**I get an import error when using _Black_ from a virtual environment**: If you get an -error message like this: - -```text -Traceback (most recent call last): - File "", line 63, in - File "/home/gui/.vim/black/lib/python3.7/site-packages/black.py", line 45, in - from typed_ast import ast3, ast27 - File "/home/gui/.vim/black/lib/python3.7/site-packages/typed_ast/ast3.py", line 40, in - from typed_ast import _ast3 -ImportError: /home/gui/.vim/black/lib/python3.7/site-packages/typed_ast/_ast3.cpython-37m-x86_64-linux-gnu.so: undefined symbool: PyExc_KeyboardInterrupt -``` - -Then you need to install `typed_ast` directly from the source code. The error happens -because `pip` will download [Python wheels](https://pythonwheels.com/) if they are -available. Python wheels are a new standard of distributing Python packages and packages -that have Cython and extensions written in C are already compiled, so the installation -is much more faster. The problem here is that somehow the Python environment inside Vim -does not match with those already compiled C extensions and these kind of errors are the -result. Luckily there is an easy fix: installing the packages from the source code. - -The package that causes problems is: - -- [typed-ast](https://pypi.org/project/typed-ast/) - -Now remove those two packages: - -```console -$ pip uninstall typed-ast -y -``` - -And now you can install them with: - -```console -$ pip install --no-binary :all: typed-ast -``` - -The C extensions will be compiled and now Vim's Python environment will match. Note that -you need to have the GCC compiler and the Python development files installed (on -Ubuntu/Debian do `sudo apt-get install build-essential python3-dev`). - -If you later want to update _Black_, you should do it like this: - -```console -$ pip install -U black --no-binary typed-ast -``` - ### With ALE 1. Install [`ale`](https://github.com/dense-analysis/ale) diff --git a/mypy.ini b/mypy.ini index 58bb7536173..95ec22d65be 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent # results. -python_version=3.7 +python_version=3.8 mypy_path=src diff --git a/pyproject.toml b/pyproject.toml index d44623fdbd3..2d8da88f6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ build-backend = "hatchling.build" name = "black" description = "The uncompromising code formatter." license = { text = "MIT" } -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "Łukasz Langa", email = "lukasz@langa.pl" }, ] @@ -69,7 +69,6 @@ dependencies = [ "pathspec>=0.9.0", "platformdirs>=2", "tomli>=1.1.0; python_version < '3.11'", - "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "typing_extensions>=3.10.0.0; python_version < '3.10'", ] dynamic = ["readme", "version"] @@ -121,8 +120,6 @@ enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", "mypy==1.3", - # Required stubs to be removed when the packages support PEP 561 themselves - "types-typed-ast>=1.4.2", ] require-runtime-dependencies = true exclude = [ @@ -145,7 +142,7 @@ options = { debug_level = "0" } [tool.cibuildwheel] build-verbosity = 1 # So these are the environments we target: -# - Python: CPython 3.7+ only +# - Python: CPython 3.8+ only # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS build = "cp3*-*" @@ -208,9 +205,6 @@ filterwarnings = [ # this is mitigated by a try/catch in https://github.com/psf/black/pull/3198/ # this ignore can be removed when support for aiohttp 3.x is dropped. '''ignore:Middleware decorator is deprecated since 4\.0 and its behaviour is default, you can simply remove this decorator:DeprecationWarning''', - # this is mitigated by https://github.com/python/cpython/issues/79071 in python 3.8+ - # this ignore can be removed when support for 3.7 is dropped. - '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', # aiohttp is using deprecated cgi modules - Safe to remove when fixed: # https://github.com/aio-libs/aiohttp/issues/6905 '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 89c202553d3..30fd32c34b0 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -52,13 +52,7 @@ def main() -> None: f.write(f"""# Generated by {basename(__file__)} # wcwidth {wcwidth.__version__} # Unicode {wcwidth.list_versions()[-1]} -import sys -from typing import List, Tuple - -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Final, List, Tuple WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [ """) diff --git a/src/black/_width_table.py b/src/black/_width_table.py index 6923f597687..f3304e48ed0 100644 --- a/src/black/_width_table.py +++ b/src/black/_width_table.py @@ -1,13 +1,7 @@ # Generated by make_width_table.py # wcwidth 0.2.6 # Unicode 15.0.0 -import sys -from typing import List, Tuple - -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Final, List, Tuple WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [ (0, 0, 0), diff --git a/src/black/brackets.py b/src/black/brackets.py index 343f0608d50..85dac6edd1e 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -1,13 +1,7 @@ """Builds on top of nodes.py to track brackets.""" -import sys from dataclasses import dataclass, field -from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union - -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Dict, Final, Iterable, List, Optional, Sequence, Set, Tuple, Union from black.nodes import ( BRACKET, diff --git a/src/black/comments.py b/src/black/comments.py index 619123ab4be..226968bff98 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,13 +1,7 @@ import re -import sys from dataclasses import dataclass from functools import lru_cache -from typing import Iterator, List, Optional, Union - -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final +from typing import Final, Iterator, List, Optional, Union from black.nodes import ( CLOSING_BRACKETS, diff --git a/src/black/mode.py b/src/black/mode.py index 1091494afac..4d979afd84d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -4,19 +4,13 @@ chosen by the user. """ -import sys from dataclasses import dataclass, field from enum import Enum, auto from hashlib import sha256 from operator import attrgetter -from typing import Dict, Set +from typing import Dict, Final, Set from warnings import warn -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final - from black.const import DEFAULT_LINE_LENGTH diff --git a/src/black/nodes.py b/src/black/nodes.py index b019b0c6440..ef42278d83f 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -3,12 +3,8 @@ """ import sys -from typing import Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union +from typing import Final, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final if sys.version_info >= (3, 10): from typing import TypeGuard else: diff --git a/src/black/parsing.py b/src/black/parsing.py index 70ed99c1549..455c5eed968 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -2,14 +2,8 @@ Parse Python code and perform AST validation. """ import ast -import platform import sys -from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union - -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Final, Iterable, Iterator, List, Set, Tuple from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature from black.nodes import syms @@ -20,25 +14,6 @@ from blib2to3.pgen2.tokenize import TokenError from blib2to3.pytree import Leaf, Node -ast3: Any - -_IS_PYPY = platform.python_implementation() == "PyPy" - -try: - from typed_ast import ast3 -except ImportError: - if sys.version_info < (3, 8) and not _IS_PYPY: - print( - "The typed_ast package is required but not installed.\n" - "You can upgrade to Python 3.8+ or install typed_ast with\n" - "`python3 -m pip install typed-ast`.", - file=sys.stderr, - ) - sys.exit(1) - else: - ast3 = ast - - PY2_HINT: Final = "Python 2 support was removed in version 22.0." @@ -147,31 +122,14 @@ def lib2to3_unparse(node: Node) -> str: def parse_single_version( src: str, version: Tuple[int, int], *, type_comments: bool -) -> Union[ast.AST, ast3.AST]: +) -> ast.AST: filename = "" - # typed-ast is needed because of feature version limitations in the builtin ast 3.8> - if sys.version_info >= (3, 8) and version >= (3,): - return ast.parse( - src, filename, feature_version=version, type_comments=type_comments - ) - - if _IS_PYPY: - # PyPy 3.7 doesn't support type comment tracking which is not ideal, but there's - # not much we can do as typed-ast won't work either. - if sys.version_info >= (3, 8): - return ast3.parse(src, filename, type_comments=type_comments) - else: - return ast3.parse(src, filename) - else: - if type_comments: - # Typed-ast is guaranteed to be used here and automatically tracks type - # comments separately. - return ast3.parse(src, filename, feature_version=version[1]) - else: - return ast.parse(src, filename) + return ast.parse( + src, filename, feature_version=version, type_comments=type_comments + ) -def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: +def parse_ast(src: str) -> ast.AST: # TODO: support Python 4+ ;) versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)] @@ -193,9 +151,6 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST]: raise SyntaxError(first_error) -ast3_AST: Final[Type[ast3.AST]] = ast3.AST - - def _normalize(lineend: str, value: str) -> str: # To normalize, we strip any leading and trailing space from # each line... @@ -206,23 +161,25 @@ def _normalize(lineend: str, value: str) -> str: return normalized.strip() -def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[str]: +def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: """Simple visitor generating strings to compare ASTs by content.""" - node = fixup_ast_constants(node) + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + and node.kind == "u" + ): + # It's a quirk of history that we strip the u prefix over here. We used to + # rewrite the AST nodes for Python version compatibility and we never copied + # over the kind + node.kind = None yield f"{' ' * depth}{node.__class__.__name__}(" - type_ignore_classes: Tuple[Type[Any], ...] for field in sorted(node._fields): # noqa: F402 - # TypeIgnore will not be present using pypy < 3.8, so need for this - if not (_IS_PYPY and sys.version_info < (3, 8)): - # TypeIgnore has only one field 'lineno' which breaks this comparison - type_ignore_classes = (ast3.TypeIgnore,) - if sys.version_info >= (3, 8): - type_ignore_classes += (ast.TypeIgnore,) - if isinstance(node, type_ignore_classes): - break + # TypeIgnore has only one field 'lineno' which breaks this comparison + if isinstance(node, ast.TypeIgnore): + break try: value: object = getattr(node, field) @@ -237,22 +194,16 @@ def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[st # parentheses and they change the AST. if ( field == "targets" - and isinstance(node, (ast.Delete, ast3.Delete)) - and isinstance(item, (ast.Tuple, ast3.Tuple)) + and isinstance(node, ast.Delete) + and isinstance(item, ast.Tuple) ): for elt in item.elts: yield from stringify_ast(elt, depth + 2) - elif isinstance(item, (ast.AST, ast3.AST)): + elif isinstance(item, ast.AST): yield from stringify_ast(item, depth + 2) - # Note that we are referencing the typed-ast ASTs via global variables and not - # direct module attribute accesses because that breaks mypyc. It's probably - # something to do with the ast3 variables being marked as Any leading - # mypy to think this branch is always taken, leaving the rest of the code - # unanalyzed. Tighting up the types for the typed-ast AST types avoids the - # mypyc crash. - elif isinstance(value, (ast.AST, ast3_AST)): + elif isinstance(value, ast.AST): yield from stringify_ast(value, depth + 2) else: @@ -271,17 +222,3 @@ def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[st yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" yield f"{' ' * depth}) # /{node.__class__.__name__}" - - -def fixup_ast_constants(node: Union[ast.AST, ast3.AST]) -> Union[ast.AST, ast3.AST]: - """Map ast nodes deprecated in 3.8 to Constant.""" - if isinstance(node, (ast.Str, ast3.Str, ast.Bytes, ast3.Bytes)): - return ast.Constant(value=node.s) - - if isinstance(node, (ast.Num, ast3.Num)): - return ast.Constant(value=node.n) - - if isinstance(node, (ast.NameConstant, ast3.NameConstant)): - return ast.Constant(value=node.value) - - return node diff --git a/src/black/strings.py b/src/black/strings.py index ac18aef51ed..0d30f09ed11 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -5,16 +5,10 @@ import re import sys from functools import lru_cache -from typing import List, Match, Pattern - -from blib2to3.pytree import Leaf - -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Final, List, Match, Pattern from black._width_table import WIDTH_TABLE +from blib2to3.pytree import Leaf STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters. STRING_PREFIX_RE: Final = re.compile( diff --git a/src/black/trans.py b/src/black/trans.py index 4d40cb4bdf6..daed26427d7 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -2,7 +2,6 @@ String transformers that can split and merge strings. """ import re -import sys from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass @@ -12,9 +11,11 @@ ClassVar, Collection, Dict, + Final, Iterable, Iterator, List, + Literal, Optional, Sequence, Set, @@ -23,11 +24,6 @@ Union, ) -if sys.version_info < (3, 8): - from typing_extensions import Final, Literal -else: - from typing import Literal, Final - from mypy_extensions import trait from black.comments import contains_pragma_comment diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index 1e0dec9c714..c939531d7c8 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -3,10 +3,7 @@ import sys from typing import Dict -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final +from typing import Final # Taken from Python (r53757) and modified to include some tokens # originally monkeypatched in by pgen2.tokenize diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 2d0cc4324ce..a5e89188d87 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -42,10 +42,7 @@ cast, ) -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final +from typing import Final from blib2to3.pgen2.token import * from blib2to3.pgen2.grammar import Grammar From 4130c65578b9ac2a42b3b18f4f38917607db3500 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 08:14:38 -0700 Subject: [PATCH 486/700] Fix CI for Click typing issue (#3770) https://github.com/pallets/click/issues/2558 --- .github/workflows/diff_shades.yml | 4 ++-- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index a126756f102..d685ef9456d 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -26,7 +26,7 @@ jobs: - name: Install diff-shades and support dependencies run: | - python -m pip install click packaging urllib3 + python -m pip install 'click==8.1.3' packaging urllib3 python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - name: Calculate run configuration & metadata @@ -64,7 +64,7 @@ jobs: - name: Install diff-shades and support dependencies run: | python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip - python -m pip install click packaging urllib3 + python -m pip install 'click==8.1.3' packaging urllib3 # After checking out old revisions, this might not exist so we'll use a copy. cat scripts/diff_shades_gha_helper.py > helper.py git config user.name "diff-shades-gha" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a69fb645238..bb647763e70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - types-PyYAML - tomli >= 0.2.6, < 2.0.0 - types-typed-ast >= 1.4.1 - - click >= 8.1.0 + - click >= 8.1.0, != 8.1.4 - packaging >= 22.0 - platformdirs >= 2.1.0 - pytest diff --git a/pyproject.toml b/pyproject.toml index 2d8da88f6c6..175f7851dee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", "mypy==1.3", + "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true exclude = [ From 114e8357e65384e17baaa3c31aa528371e15679b Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:29:47 -0700 Subject: [PATCH 487/700] Remove click patch (#3768) Apparently this was only needed on Python 3.6. We've now dropped support for 3.6 and 3.7. It's also not needed on new enough click. --- CHANGES.md | 1 + .../reference/reference_functions.rst | 2 -- src/black/__init__.py | 35 ------------------- src/blackd/__init__.py | 1 - tests/test_black.py | 24 ------------- 5 files changed, 1 insertion(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 24ca54a82ac..1b0475fc7b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,6 +43,7 @@ - Upgrade mypyc from 0.991 to 1.3 (#3697) +- Remove patching of Click that mitigated errors on Python 3.6 with `LANG=C` (#3768) ### Parser diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index 3bda5de1774..09517f73961 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -165,8 +165,6 @@ Utilities .. autofunction:: black.linegen.normalize_invisible_parens -.. autofunction:: black.patch_click - .. autofunction:: black.nodes.preceding_leaf .. autofunction:: black.re_compile_maybe_verbose diff --git a/src/black/__init__.py b/src/black/__init__.py index b6611bef84b..301c18f7338 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1410,40 +1410,6 @@ def nullcontext() -> Iterator[None]: yield -def patch_click() -> None: - """Make Click not crash on Python 3.6 with LANG=C. - - On certain misconfigured environments, Python 3 selects the ASCII encoding as the - default which restricts paths that it can access during the lifetime of the - application. Click refuses to work in this scenario by raising a RuntimeError. - - In case of Black the likelihood that non-ASCII characters are going to be used in - file paths is minimal since it's Python source code. Moreover, this crash was - spurious on Python 3.7 thanks to PEP 538 and PEP 540. - """ - modules: List[Any] = [] - try: - from click import core - except ImportError: - pass - else: - modules.append(core) - try: - # Removed in Click 8.1.0 and newer; we keep this around for users who have - # older versions installed. - from click import _unicodefun # type: ignore - except ImportError: - pass - else: - modules.append(_unicodefun) - - for module in modules: - if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None - if hasattr(module, "_verify_python_env"): - module._verify_python_env = lambda: None - - def patched_main() -> None: # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows # environments so just assume we always need to call it if frozen. @@ -1452,7 +1418,6 @@ def patched_main() -> None: freeze_support() - patch_click() main() diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index c1b69feed63..4f2d87d0fca 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -225,7 +225,6 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi def patched_main() -> None: maybe_install_uvloop() freeze_support() - black.patch_click() main() diff --git a/tests/test_black.py b/tests/test_black.py index dee6ead50d0..dd21d0a7ae6 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1455,30 +1455,6 @@ def test_assert_equivalent_different_asts(self) -> None: with self.assertRaises(AssertionError): black.assert_equivalent("{}", "None") - def test_shhh_click(self) -> None: - try: - from click import _unicodefun # type: ignore - except ImportError: - self.skipTest("Incompatible Click version") - - if not hasattr(_unicodefun, "_verify_python_env"): - self.skipTest("Incompatible Click version") - - # First, let's see if Click is crashing with a preferred ASCII charset. - with patch("locale.getpreferredencoding") as gpe: - gpe.return_value = "ASCII" - with self.assertRaises(RuntimeError): - _unicodefun._verify_python_env() - # Now, let's silence Click... - black.patch_click() - # ...and confirm it's silent. - with patch("locale.getpreferredencoding") as gpe: - gpe.return_value = "ASCII" - try: - _unicodefun._verify_python_env() - except RuntimeError as re: - self.fail(f"`patch_click()` failed, exception still raised: {re}") - def test_root_logger_not_used_directly(self) -> None: def fail(*args: Any, **kwargs: Any) -> None: self.fail("Record created with root logger") From 0b4d7d55f78913be9e0a3738681ef3aafd5d9a5a Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:05:01 -0700 Subject: [PATCH 488/700] Run pyupgrade on blib2to3 and src (#3771) --- src/black/files.py | 6 +-- src/black/handle_ipynb_magics.py | 2 +- src/blib2to3/pgen2/conv.py | 6 +-- src/blib2to3/pgen2/driver.py | 29 +++++++------- src/blib2to3/pgen2/grammar.py | 6 +-- src/blib2to3/pgen2/literals.py | 8 ++-- src/blib2to3/pgen2/parse.py | 22 +++++------ src/blib2to3/pgen2/pgen.py | 35 ++++++++--------- src/blib2to3/pgen2/token.py | 3 +- src/blib2to3/pgen2/tokenize.py | 27 +++++++------ src/blib2to3/pygram.py | 3 +- src/blib2to3/pytree.py | 67 ++++++++++++++++---------------- 12 files changed, 102 insertions(+), 112 deletions(-) diff --git a/src/black/files.py b/src/black/files.py index 65b2d0a8402..4e2209e557d 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -42,7 +42,7 @@ import colorama # noqa: F401 -@lru_cache() +@lru_cache def find_project_root( srcs: Sequence[str], stdin_filename: Optional[str] = None ) -> Tuple[Path, str]: @@ -212,7 +212,7 @@ def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: return SpecifierSet(",".join(str(s) for s in specifiers)) -@lru_cache() +@lru_cache def find_user_pyproject_toml() -> Path: r"""Return the path to the top-level user configuration for black. @@ -232,7 +232,7 @@ def find_user_pyproject_toml() -> Path: return user_config_path.resolve() -@lru_cache() +@lru_cache def get_gitignore(root: Path) -> PathSpec: """Return a PathSpec matching gitignore content if present.""" gitignore = root / ".gitignore" diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 4324819beaf..2a2d62220e2 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -55,7 +55,7 @@ class Replacement: src: str -@lru_cache() +@lru_cache def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: try: # isort: off diff --git a/src/blib2to3/pgen2/conv.py b/src/blib2to3/pgen2/conv.py index fa9825e54d6..04eccfa1d4b 100644 --- a/src/blib2to3/pgen2/conv.py +++ b/src/blib2to3/pgen2/conv.py @@ -63,7 +63,7 @@ def parse_graminit_h(self, filename): try: f = open(filename) except OSError as err: - print("Can't open %s: %s" % (filename, err)) + print(f"Can't open {filename}: {err}") return False self.symbol2number = {} self.number2symbol = {} @@ -72,7 +72,7 @@ def parse_graminit_h(self, filename): lineno += 1 mo = re.match(r"^#define\s+(\w+)\s+(\d+)$", line) if not mo and line.strip(): - print("%s(%s): can't parse %s" % (filename, lineno, line.strip())) + print(f"{filename}({lineno}): can't parse {line.strip()}") else: symbol, number = mo.groups() number = int(number) @@ -113,7 +113,7 @@ def parse_graminit_c(self, filename): try: f = open(filename) except OSError as err: - print("Can't open %s: %s" % (filename, err)) + print(f"Can't open {filename}: {err}") return False # The code below essentially uses f's iterator-ness! lineno = 0 diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 1741b33c510..bb73016a4c1 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -28,11 +28,8 @@ Iterable, List, Optional, - Text, Iterator, Tuple, - TypeVar, - Generic, Union, ) from contextlib import contextmanager @@ -116,7 +113,7 @@ def can_advance(self, to: int) -> bool: return True -class Driver(object): +class Driver: def __init__(self, grammar: Grammar, logger: Optional[Logger] = None) -> None: self.grammar = grammar if logger is None: @@ -189,30 +186,30 @@ def parse_tokens(self, tokens: Iterable[GoodTokenInfo], debug: bool = False) -> assert p.rootnode is not None return p.rootnode - def parse_stream_raw(self, stream: IO[Text], debug: bool = False) -> NL: + def parse_stream_raw(self, stream: IO[str], debug: bool = False) -> NL: """Parse a stream and return the syntax tree.""" tokens = tokenize.generate_tokens(stream.readline, grammar=self.grammar) return self.parse_tokens(tokens, debug) - def parse_stream(self, stream: IO[Text], debug: bool = False) -> NL: + def parse_stream(self, stream: IO[str], debug: bool = False) -> NL: """Parse a stream and return the syntax tree.""" return self.parse_stream_raw(stream, debug) def parse_file( - self, filename: Path, encoding: Optional[Text] = None, debug: bool = False + self, filename: Path, encoding: Optional[str] = None, debug: bool = False ) -> NL: """Parse a file and return the syntax tree.""" - with io.open(filename, "r", encoding=encoding) as stream: + with open(filename, encoding=encoding) as stream: return self.parse_stream(stream, debug) - def parse_string(self, text: Text, debug: bool = False) -> NL: + def parse_string(self, text: str, debug: bool = False) -> NL: """Parse a string and return the syntax tree.""" tokens = tokenize.generate_tokens( io.StringIO(text).readline, grammar=self.grammar ) return self.parse_tokens(tokens, debug) - def _partially_consume_prefix(self, prefix: Text, column: int) -> Tuple[Text, Text]: + def _partially_consume_prefix(self, prefix: str, column: int) -> Tuple[str, str]: lines: List[str] = [] current_line = "" current_column = 0 @@ -240,7 +237,7 @@ def _partially_consume_prefix(self, prefix: Text, column: int) -> Tuple[Text, Te return "".join(lines), current_line -def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> Text: +def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> str: head, tail = os.path.splitext(gt) if tail == ".txt": tail = "" @@ -252,8 +249,8 @@ def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> Text: def load_grammar( - gt: Text = "Grammar.txt", - gp: Optional[Text] = None, + gt: str = "Grammar.txt", + gp: Optional[str] = None, save: bool = True, force: bool = False, logger: Optional[Logger] = None, @@ -276,7 +273,7 @@ def load_grammar( return g -def _newer(a: Text, b: Text) -> bool: +def _newer(a: str, b: str) -> bool: """Inquire whether file a was written since file b.""" if not os.path.exists(a): return False @@ -286,7 +283,7 @@ def _newer(a: Text, b: Text) -> bool: def load_packaged_grammar( - package: str, grammar_source: Text, cache_dir: Optional[Path] = None + package: str, grammar_source: str, cache_dir: Optional[Path] = None ) -> grammar.Grammar: """Normally, loads a pickled grammar by doing pkgutil.get_data(package, pickled_grammar) @@ -309,7 +306,7 @@ def load_packaged_grammar( return g -def main(*args: Text) -> bool: +def main(*args: str) -> bool: """Main program, when run as a script: produce grammar pickle files. Calls load_grammar for each argument, a path to a grammar text file. diff --git a/src/blib2to3/pgen2/grammar.py b/src/blib2to3/pgen2/grammar.py index 337a64f1726..1f3fdc55b97 100644 --- a/src/blib2to3/pgen2/grammar.py +++ b/src/blib2to3/pgen2/grammar.py @@ -16,19 +16,19 @@ import os import pickle import tempfile -from typing import Any, Dict, List, Optional, Text, Tuple, TypeVar, Union +from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union # Local imports from . import token _P = TypeVar("_P", bound="Grammar") -Label = Tuple[int, Optional[Text]] +Label = Tuple[int, Optional[str]] DFA = List[List[Tuple[int, int]]] DFAS = Tuple[DFA, Dict[int, int]] Path = Union[str, "os.PathLike[str]"] -class Grammar(object): +class Grammar: """Pgen parsing tables conversion class. Once initialized, this class supplies the grammar tables for the diff --git a/src/blib2to3/pgen2/literals.py b/src/blib2to3/pgen2/literals.py index b5fe4285114..c67b91d0463 100644 --- a/src/blib2to3/pgen2/literals.py +++ b/src/blib2to3/pgen2/literals.py @@ -5,10 +5,10 @@ import re -from typing import Dict, Match, Text +from typing import Dict, Match -simple_escapes: Dict[Text, Text] = { +simple_escapes: Dict[str, str] = { "a": "\a", "b": "\b", "f": "\f", @@ -22,7 +22,7 @@ } -def escape(m: Match[Text]) -> Text: +def escape(m: Match[str]) -> str: all, tail = m.group(0, 1) assert all.startswith("\\") esc = simple_escapes.get(tail) @@ -44,7 +44,7 @@ def escape(m: Match[Text]) -> Text: return chr(i) -def evalString(s: Text) -> Text: +def evalString(s: str) -> str: assert s.startswith("'") or s.startswith('"'), repr(s[:1]) q = s[0] if s[:3] == q * 3: diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index c462f63ad2c..17bf118e9fc 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -9,7 +9,6 @@ how this parsing engine works. """ -import copy from contextlib import contextmanager # Local imports @@ -18,7 +17,6 @@ cast, Any, Optional, - Text, Union, Tuple, Dict, @@ -35,7 +33,7 @@ from blib2to3.pgen2.driver import TokenProxy -Results = Dict[Text, NL] +Results = Dict[str, NL] Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]] DFA = List[List[Tuple[int, int]]] DFAS = Tuple[DFA, Dict[int, int]] @@ -100,7 +98,7 @@ def backtrack(self) -> Iterator[None]: finally: self.parser.is_backtracking = is_backtracking - def add_token(self, tok_type: int, tok_val: Text, raw: bool = False) -> None: + def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None: func: Callable[..., Any] if raw: func = self.parser._addtoken @@ -114,7 +112,7 @@ def add_token(self, tok_type: int, tok_val: Text, raw: bool = False) -> None: args.insert(0, ilabel) func(*args) - def determine_route(self, value: Optional[Text] = None, force: bool = False) -> Optional[int]: + def determine_route(self, value: Optional[str] = None, force: bool = False) -> Optional[int]: alive_ilabels = self.ilabels if len(alive_ilabels) == 0: *_, most_successful_ilabel = self._dead_ilabels @@ -131,10 +129,10 @@ class ParseError(Exception): """Exception to signal the parser is stuck.""" def __init__( - self, msg: Text, type: Optional[int], value: Optional[Text], context: Context + self, msg: str, type: Optional[int], value: Optional[str], context: Context ) -> None: Exception.__init__( - self, "%s: type=%r, value=%r, context=%r" % (msg, type, value, context) + self, f"{msg}: type={type!r}, value={value!r}, context={context!r}" ) self.msg = msg self.type = type @@ -142,7 +140,7 @@ def __init__( self.context = context -class Parser(object): +class Parser: """Parser engine. The proper usage sequence is: @@ -236,7 +234,7 @@ def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: self.used_names: Set[str] = set() self.proxy = proxy - def addtoken(self, type: int, value: Text, context: Context) -> bool: + def addtoken(self, type: int, value: str, context: Context) -> bool: """Add a token; return True iff this is the end of the program.""" # Map from token to label ilabels = self.classify(type, value, context) @@ -284,7 +282,7 @@ def addtoken(self, type: int, value: Text, context: Context) -> bool: return self._addtoken(ilabel, type, value, context) - def _addtoken(self, ilabel: int, type: int, value: Text, context: Context) -> bool: + def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> bool: # Loop until the token is shifted; may raise exceptions while True: dfa, state, node = self.stack[-1] @@ -329,7 +327,7 @@ def _addtoken(self, ilabel: int, type: int, value: Text, context: Context) -> bo # No success finding a transition raise ParseError("bad input", type, value, context) - def classify(self, type: int, value: Text, context: Context) -> List[int]: + def classify(self, type: int, value: str, context: Context) -> List[int]: """Turn a token into a label. (Internal) Depending on whether the value is a soft-keyword or not, @@ -352,7 +350,7 @@ def classify(self, type: int, value: Text, context: Context) -> List[int]: raise ParseError("bad token", type, value, context) return [ilabel] - def shift(self, type: int, value: Text, newstate: int, context: Context) -> None: + def shift(self, type: int, value: str, newstate: int, context: Context) -> None: """Shift a token. (Internal)""" if self.is_backtracking: dfa, state, _ = self.stack[-1] diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index b5ebc7b3e42..046efd09338 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -11,7 +11,6 @@ Iterator, List, Optional, - Text, Tuple, Union, Sequence, @@ -29,13 +28,13 @@ class PgenGrammar(grammar.Grammar): pass -class ParserGenerator(object): +class ParserGenerator: filename: Path - stream: IO[Text] + stream: IO[str] generator: Iterator[GoodTokenInfo] - first: Dict[Text, Optional[Dict[Text, int]]] + first: Dict[str, Optional[Dict[str, int]]] - def __init__(self, filename: Path, stream: Optional[IO[Text]] = None) -> None: + def __init__(self, filename: Path, stream: Optional[IO[str]] = None) -> None: close_stream = None if stream is None: stream = open(filename, encoding="utf-8") @@ -75,7 +74,7 @@ def make_grammar(self) -> PgenGrammar: c.start = c.symbol2number[self.startsymbol] return c - def make_first(self, c: PgenGrammar, name: Text) -> Dict[int, int]: + def make_first(self, c: PgenGrammar, name: str) -> Dict[int, int]: rawfirst = self.first[name] assert rawfirst is not None first = {} @@ -85,7 +84,7 @@ def make_first(self, c: PgenGrammar, name: Text) -> Dict[int, int]: first[ilabel] = 1 return first - def make_label(self, c: PgenGrammar, label: Text) -> int: + def make_label(self, c: PgenGrammar, label: str) -> int: # XXX Maybe this should be a method on a subclass of converter? ilabel = len(c.labels) if label[0].isalpha(): @@ -144,7 +143,7 @@ def addfirstsets(self) -> None: self.calcfirst(name) # print name, self.first[name].keys() - def calcfirst(self, name: Text) -> None: + def calcfirst(self, name: str) -> None: dfa = self.dfas[name] self.first[name] = None # dummy to detect left recursion state = dfa[0] @@ -176,7 +175,7 @@ def calcfirst(self, name: Text) -> None: inverse[symbol] = label self.first[name] = totalset - def parse(self) -> Tuple[Dict[Text, List["DFAState"]], Text]: + def parse(self) -> Tuple[Dict[str, List["DFAState"]], str]: dfas = {} startsymbol: Optional[str] = None # MSTART: (NEWLINE | RULE)* ENDMARKER @@ -240,7 +239,7 @@ def addclosure(state: NFAState, base: Dict[NFAState, int]) -> None: state.addarc(st, label) return states # List of DFAState instances; first one is start - def dump_nfa(self, name: Text, start: "NFAState", finish: "NFAState") -> None: + def dump_nfa(self, name: str, start: "NFAState", finish: "NFAState") -> None: print("Dump of NFA for", name) todo = [start] for i, state in enumerate(todo): @@ -256,7 +255,7 @@ def dump_nfa(self, name: Text, start: "NFAState", finish: "NFAState") -> None: else: print(" %s -> %d" % (label, j)) - def dump_dfa(self, name: Text, dfa: Sequence["DFAState"]) -> None: + def dump_dfa(self, name: str, dfa: Sequence["DFAState"]) -> None: print("Dump of DFA for", name) for i, state in enumerate(dfa): print(" State", i, state.isfinal and "(final)" or "") @@ -349,7 +348,7 @@ def parse_atom(self) -> Tuple["NFAState", "NFAState"]: ) assert False - def expect(self, type: int, value: Optional[Any] = None) -> Text: + def expect(self, type: int, value: Optional[Any] = None) -> str: if self.type != type or (value is not None and self.value != value): self.raise_error( "expected %s/%s, got %s/%s", type, value, self.type, self.value @@ -374,22 +373,22 @@ def raise_error(self, msg: str, *args: Any) -> NoReturn: raise SyntaxError(msg, (self.filename, self.end[0], self.end[1], self.line)) -class NFAState(object): - arcs: List[Tuple[Optional[Text], "NFAState"]] +class NFAState: + arcs: List[Tuple[Optional[str], "NFAState"]] def __init__(self) -> None: self.arcs = [] # list of (label, NFAState) pairs - def addarc(self, next: "NFAState", label: Optional[Text] = None) -> None: + def addarc(self, next: "NFAState", label: Optional[str] = None) -> None: assert label is None or isinstance(label, str) assert isinstance(next, NFAState) self.arcs.append((label, next)) -class DFAState(object): +class DFAState: nfaset: Dict[NFAState, Any] isfinal: bool - arcs: Dict[Text, "DFAState"] + arcs: Dict[str, "DFAState"] def __init__(self, nfaset: Dict[NFAState, Any], final: NFAState) -> None: assert isinstance(nfaset, dict) @@ -399,7 +398,7 @@ def __init__(self, nfaset: Dict[NFAState, Any], final: NFAState) -> None: self.isfinal = final in nfaset self.arcs = {} # map from label to DFAState - def addarc(self, next: "DFAState", label: Text) -> None: + def addarc(self, next: "DFAState", label: str) -> None: assert isinstance(label, str) assert label not in self.arcs assert isinstance(next, DFAState) diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index c939531d7c8..117cc09d4ce 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -1,6 +1,5 @@ """Token constants (from "token.h").""" -import sys from typing import Dict from typing import Final @@ -75,7 +74,7 @@ tok_name: Final[Dict[int, str]] = {} for _name, _value in list(globals().items()): - if type(_value) is type(0): + if type(_value) is int: tok_name[_value] = _name diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index a5e89188d87..1dea89d7bb8 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -35,7 +35,6 @@ List, Optional, Set, - Text, Tuple, Pattern, Union, @@ -77,7 +76,7 @@ def maybe(*choices: str) -> str: def _combinations(*l: str) -> Set[str]: - return set(x + y for x in l for y in l + ("",) if x.casefold() != y.casefold()) + return {x + y for x in l for y in l + ("",) if x.casefold() != y.casefold()} Whitespace = r"[ \f\t]*" @@ -189,7 +188,7 @@ class StopTokenizing(Exception): def printtoken( - type: int, token: Text, srow_col: Coord, erow_col: Coord, line: Text + type: int, token: str, srow_col: Coord, erow_col: Coord, line: str ) -> None: # for testing (srow, scol) = srow_col (erow, ecol) = erow_col @@ -198,10 +197,10 @@ def printtoken( ) -TokenEater = Callable[[int, Text, Coord, Coord, Text], None] +TokenEater = Callable[[int, str, Coord, Coord, str], None] -def tokenize(readline: Callable[[], Text], tokeneater: TokenEater = printtoken) -> None: +def tokenize(readline: Callable[[], str], tokeneater: TokenEater = printtoken) -> None: """ The tokenize() function accepts two parameters: one representing the input stream, and one providing an output mechanism for tokenize(). @@ -221,17 +220,17 @@ def tokenize(readline: Callable[[], Text], tokeneater: TokenEater = printtoken) # backwards compatible interface -def tokenize_loop(readline: Callable[[], Text], tokeneater: TokenEater) -> None: +def tokenize_loop(readline: Callable[[], str], tokeneater: TokenEater) -> None: for token_info in generate_tokens(readline): tokeneater(*token_info) -GoodTokenInfo = Tuple[int, Text, Coord, Coord, Text] +GoodTokenInfo = Tuple[int, str, Coord, Coord, str] TokenInfo = Union[Tuple[int, str], GoodTokenInfo] class Untokenizer: - tokens: List[Text] + tokens: List[str] prev_row: int prev_col: int @@ -247,13 +246,13 @@ def add_whitespace(self, start: Coord) -> None: if col_offset: self.tokens.append(" " * col_offset) - def untokenize(self, iterable: Iterable[TokenInfo]) -> Text: + def untokenize(self, iterable: Iterable[TokenInfo]) -> str: for t in iterable: if len(t) == 2: self.compat(cast(Tuple[int, str], t), iterable) break tok_type, token, start, end, line = cast( - Tuple[int, Text, Coord, Coord, Text], t + Tuple[int, str, Coord, Coord, str], t ) self.add_whitespace(start) self.tokens.append(token) @@ -263,7 +262,7 @@ def untokenize(self, iterable: Iterable[TokenInfo]) -> Text: self.prev_col = 0 return "".join(self.tokens) - def compat(self, token: Tuple[int, Text], iterable: Iterable[TokenInfo]) -> None: + def compat(self, token: Tuple[int, str], iterable: Iterable[TokenInfo]) -> None: startline = False indents = [] toks_append = self.tokens.append @@ -335,7 +334,7 @@ def read_or_stop() -> bytes: try: return readline() except StopIteration: - return bytes() + return b'' def find_cookie(line: bytes) -> Optional[str]: try: @@ -384,7 +383,7 @@ def find_cookie(line: bytes) -> Optional[str]: return default, [first, second] -def untokenize(iterable: Iterable[TokenInfo]) -> Text: +def untokenize(iterable: Iterable[TokenInfo]) -> str: """Transform tokens back into Python source code. Each element returned by the iterable must be a token sequence @@ -407,7 +406,7 @@ def untokenize(iterable: Iterable[TokenInfo]) -> Text: def generate_tokens( - readline: Callable[[], Text], grammar: Optional[Grammar] = None + readline: Callable[[], str], grammar: Optional[Grammar] = None ) -> Iterator[GoodTokenInfo]: """ The generate_tokens() generator requires one argument, readline, which diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index 15702e4059e..1b4832362bf 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -9,7 +9,6 @@ from typing import Union # Local imports -from .pgen2 import token from .pgen2 import driver from .pgen2.grammar import Grammar @@ -21,7 +20,7 @@ # "PatternGrammar.txt") -class Symbols(object): +class Symbols: def __init__(self, grammar: Grammar) -> None: """Initializer. diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index ea60c894e20..156322cab7e 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -18,7 +18,6 @@ Iterator, List, Optional, - Text, Tuple, TypeVar, Union, @@ -34,10 +33,10 @@ HUGE: int = 0x7FFFFFFF # maximum repeat count, default max -_type_reprs: Dict[int, Union[Text, int]] = {} +_type_reprs: Dict[int, Union[str, int]] = {} -def type_repr(type_num: int) -> Union[Text, int]: +def type_repr(type_num: int) -> Union[str, int]: global _type_reprs if not _type_reprs: from .pygram import python_symbols @@ -54,11 +53,11 @@ def type_repr(type_num: int) -> Union[Text, int]: _P = TypeVar("_P", bound="Base") NL = Union["Node", "Leaf"] -Context = Tuple[Text, Tuple[int, int]] -RawNode = Tuple[int, Optional[Text], Optional[Context], Optional[List[NL]]] +Context = Tuple[str, Tuple[int, int]] +RawNode = Tuple[int, Optional[str], Optional[Context], Optional[List[NL]]] -class Base(object): +class Base: """ Abstract base class for Node and Leaf. @@ -92,7 +91,7 @@ def __eq__(self, other: Any) -> bool: return self._eq(other) @property - def prefix(self) -> Text: + def prefix(self) -> str: raise NotImplementedError def _eq(self: _P, other: _P) -> bool: @@ -225,7 +224,7 @@ def depth(self) -> int: return 0 return 1 + self.parent.depth() - def get_suffix(self) -> Text: + def get_suffix(self) -> str: """ Return the string immediately following the invocant node. This is effectively equivalent to node.next_sibling.prefix @@ -242,14 +241,14 @@ class Node(Base): """Concrete implementation for interior nodes.""" fixers_applied: Optional[List[Any]] - used_names: Optional[Set[Text]] + used_names: Optional[Set[str]] def __init__( self, type: int, children: List[NL], context: Optional[Any] = None, - prefix: Optional[Text] = None, + prefix: Optional[str] = None, fixers_applied: Optional[List[Any]] = None, ) -> None: """ @@ -274,16 +273,16 @@ def __init__( else: self.fixers_applied = None - def __repr__(self) -> Text: + def __repr__(self) -> str: """Return a canonical string representation.""" assert self.type is not None - return "%s(%s, %r)" % ( + return "{}({}, {!r})".format( self.__class__.__name__, type_repr(self.type), self.children, ) - def __str__(self) -> Text: + def __str__(self) -> str: """ Return a pretty string representation. @@ -317,7 +316,7 @@ def pre_order(self) -> Iterator[NL]: yield from child.pre_order() @property - def prefix(self) -> Text: + def prefix(self) -> str: """ The whitespace and comments preceding this node in the input. """ @@ -326,7 +325,7 @@ def prefix(self) -> Text: return self.children[0].prefix @prefix.setter - def prefix(self, prefix: Text) -> None: + def prefix(self, prefix: str) -> None: if self.children: self.children[0].prefix = prefix @@ -383,12 +382,12 @@ class Leaf(Base): """Concrete implementation for leaf nodes.""" # Default values for instance variables - value: Text + value: str fixers_applied: List[Any] bracket_depth: int # Changed later in brackets.py opening_bracket: Optional["Leaf"] = None - used_names: Optional[Set[Text]] + used_names: Optional[Set[str]] _prefix = "" # Whitespace and comments preceding this token in the input lineno: int = 0 # Line where this token starts in the input column: int = 0 # Column where this token starts in the input @@ -400,9 +399,9 @@ class Leaf(Base): def __init__( self, type: int, - value: Text, + value: str, context: Optional[Context] = None, - prefix: Optional[Text] = None, + prefix: Optional[str] = None, fixers_applied: List[Any] = [], opening_bracket: Optional["Leaf"] = None, fmt_pass_converted_first_leaf: Optional["Leaf"] = None, @@ -431,13 +430,13 @@ def __repr__(self) -> str: from .pgen2.token import tok_name assert self.type is not None - return "%s(%s, %r)" % ( + return "{}({}, {!r})".format( self.__class__.__name__, tok_name.get(self.type, self.type), self.value, ) - def __str__(self) -> Text: + def __str__(self) -> str: """ Return a pretty string representation. @@ -471,14 +470,14 @@ def pre_order(self) -> Iterator["Leaf"]: yield self @property - def prefix(self) -> Text: + def prefix(self) -> str: """ The whitespace and comments preceding this token in the input. """ return self._prefix @prefix.setter - def prefix(self, prefix: Text) -> None: + def prefix(self, prefix: str) -> None: self.changed() self._prefix = prefix @@ -503,10 +502,10 @@ def convert(gr: Grammar, raw_node: RawNode) -> NL: return Leaf(type, value or "", context=context) -_Results = Dict[Text, NL] +_Results = Dict[str, NL] -class BasePattern(object): +class BasePattern: """ A pattern is a tree matching pattern. @@ -526,19 +525,19 @@ class BasePattern(object): type: Optional[int] type = None # Node type (token if < 256, symbol if >= 256) content: Any = None # Optional content matching pattern - name: Optional[Text] = None # Optional name used to store match in results dict + name: Optional[str] = None # Optional name used to store match in results dict def __new__(cls, *args, **kwds): """Constructor that prevents BasePattern from being instantiated.""" assert cls is not BasePattern, "Cannot instantiate BasePattern" return object.__new__(cls) - def __repr__(self) -> Text: + def __repr__(self) -> str: assert self.type is not None args = [type_repr(self.type), self.content, self.name] while args and args[-1] is None: del args[-1] - return "%s(%s)" % (self.__class__.__name__, ", ".join(map(repr, args))) + return "{}({})".format(self.__class__.__name__, ", ".join(map(repr, args))) def _submatch(self, node, results=None) -> bool: raise NotImplementedError @@ -602,8 +601,8 @@ class LeafPattern(BasePattern): def __init__( self, type: Optional[int] = None, - content: Optional[Text] = None, - name: Optional[Text] = None, + content: Optional[str] = None, + name: Optional[str] = None, ) -> None: """ Initializer. Takes optional type, content, and name. @@ -653,8 +652,8 @@ class NodePattern(BasePattern): def __init__( self, type: Optional[int] = None, - content: Optional[Iterable[Text]] = None, - name: Optional[Text] = None, + content: Optional[Iterable[str]] = None, + name: Optional[str] = None, ) -> None: """ Initializer. Takes optional type, content, and name. @@ -734,10 +733,10 @@ class WildcardPattern(BasePattern): def __init__( self, - content: Optional[Text] = None, + content: Optional[str] = None, min: int = 0, max: int = HUGE, - name: Optional[Text] = None, + name: Optional[str] = None, ) -> None: """ Initializer. From f3b50e466969f9142393ec32a4b2a383ffbe5f23 Mon Sep 17 00:00:00 2001 From: Kenneth Schackart Date: Sun, 9 Jul 2023 15:07:21 -0700 Subject: [PATCH 489/700] Add CITATION.cff file (#3723) --- CHANGES.md | 3 +++ CITATION.cff | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 CITATION.cff diff --git a/CHANGES.md b/CHANGES.md index 1b0475fc7b9..15027afbf0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -84,6 +84,9 @@ ### Documentation +- Add a CITATION.cff file to the root of the repository, containing metadata on how to + cite this software (#3723) + diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000000..ddf64f616ff --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,22 @@ +cff-version: 1.2.0 +title: "Black: The uncompromising Python code formatter" +message: >- + If you use this software, please cite it using the metadata from this file. +type: software +authors: + - family-names: Langa + given-names: Łukasz + - name: "contributors to Black" +repository-code: "https://github.com/psf/black" +url: "https://black.readthedocs.io/en/stable/" +abstract: >- + Black is the uncompromising Python code formatter. By using it, you agree to cede + control over minutiae ofhand-formatting. In return, Black gives you speed, + determinism, and freedom from pycodestyle nagging about formatting. You will save time + and mental energy for more important matters. + + Blackened code looks the same regardless of the project you're reading. Formatting + becomes transparent after a while and you can focus on the content instead. + + Black makes code review faster by producing the smallest diffs possible. +license: MIT From 2593af2c5d211b58e28f7c1472f1f67e6783216a Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:24:01 -0700 Subject: [PATCH 490/700] Improve performance by skipping unnecessary normalisation (#3751) This speeds up black by about 40% when the cache is full --- CHANGES.md | 1 + src/black/files.py | 23 +++++++++++++++++------ tests/test_black.py | 4 +++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 15027afbf0b..93d8ee1921a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,6 +55,7 @@ +- Speed up _Black_ significantly when the cache is full (#3751) - Avoid importing `IPython` in a case where we wouldn't need it (#3748) ### Output diff --git a/src/black/files.py b/src/black/files.py index 4e2209e557d..ef6895ee3af 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -276,15 +276,24 @@ def normalize_path_maybe_ignore( return root_relative_path -def path_is_ignored( - path: Path, gitignore_dict: Dict[Path, PathSpec], report: Report +def _path_is_ignored( + root_relative_path: str, + root: Path, + gitignore_dict: Dict[Path, PathSpec], + report: Report, ) -> bool: + path = root / root_relative_path + # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must + # ensure that gitignore_dict is ordered from least specific to most specific. for gitignore_path, pattern in gitignore_dict.items(): - relative_path = normalize_path_maybe_ignore(path, gitignore_path, report) - if relative_path is None: + try: + relative_path = path.relative_to(gitignore_path).as_posix() + except ValueError: break if pattern.match_file(relative_path): - report.path_ignored(path, "matches a .gitignore file content") + report.path_ignored( + path.relative_to(root), "matches a .gitignore file content" + ) return True return False @@ -326,7 +335,9 @@ def gen_python_files( continue # First ignore files matching .gitignore, if passed - if gitignore_dict and path_is_ignored(child, gitignore_dict, report): + if gitignore_dict and _path_is_ignored( + normalized_path, root, gitignore_dict, report + ): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. diff --git a/tests/test_black.py b/tests/test_black.py index dd21d0a7ae6..3b3ab721c5f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -508,6 +508,8 @@ def _mocked_calls() -> bool: "pathlib.Path.cwd", return_value=working_directory ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])): ctx = FakeContext() + # Note that the root folder (project_root) isn't the folder + # named "root" (aka working_directory) ctx.obj["root"] = project_root report = MagicMock(verbose=True) black.get_sources( @@ -527,7 +529,7 @@ def _mocked_calls() -> bool: for _, mock_args, _ in report.path_ignored.mock_calls ), "A symbolic link was reported." report.path_ignored.assert_called_once_with( - Path("child", "b.py"), "matches a .gitignore file content" + Path("root", "child", "b.py"), "matches a .gitignore file content" ) def test_report_verbose(self) -> None: From 257d392217974a76231e306133288748c7b70786 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 9 Jul 2023 15:52:41 -0700 Subject: [PATCH 491/700] Fix removed comments in stub files (#3745) --- CHANGES.md | 2 ++ src/black/nodes.py | 11 ++++++- tests/data/simple_cases/ignore_pyi.py | 41 +++++++++++++++++++++++++++ tests/test_format.py | 5 ++-- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/data/simple_cases/ignore_pyi.py diff --git a/CHANGES.md b/CHANGES.md index 93d8ee1921a..bb304296d63 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Fix a bug where an illegal trailing comma was added to return type annotations using PEP 604 unions (#3735) +- Fix several bugs and crashes where comments in stub files were removed or mishandled + under some circumstances. (#3745) - Fix a bug where multi-line open parenthesis magic comment like `type: ignore` were not correctly parsed (#3740) diff --git a/src/black/nodes.py b/src/black/nodes.py index ef42278d83f..45423b2596b 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -714,6 +714,11 @@ def is_multiline_string(leaf: Leaf) -> bool: def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" + + # If there is a comment, we want to keep it. + if node.prefix.strip(): + return False + if ( len(node.children) != 4 or node.children[0].type != token.NEWLINE @@ -722,6 +727,9 @@ def is_stub_suite(node: Node) -> bool: ): return False + if node.children[3].prefix.strip(): + return False + return is_stub_body(node.children[2]) @@ -735,7 +743,8 @@ def is_stub_body(node: LN) -> bool: child = node.children[0] return ( - child.type == syms.atom + not child.prefix.strip() + and child.type == syms.atom and len(child.children) == 3 and all(leaf == Leaf(token.DOT, ".") for leaf in child.children) ) diff --git a/tests/data/simple_cases/ignore_pyi.py b/tests/data/simple_cases/ignore_pyi.py new file mode 100644 index 00000000000..3ef61079bfe --- /dev/null +++ b/tests/data/simple_cases/ignore_pyi.py @@ -0,0 +1,41 @@ +def f(): # type: ignore + ... + +class x: # some comment + ... + +class y: + ... # comment + +# whitespace doesn't matter (note the next line has a trailing space and tab) +class z: + ... + +def g(): + # hi + ... + +def h(): + ... + # bye + +# output + +def f(): # type: ignore + ... + +class x: # some comment + ... + +class y: ... # comment + +# whitespace doesn't matter (note the next line has a trailing space and tab) +class z: ... + +def g(): + # hi + ... + +def h(): + ... + # bye diff --git a/tests/test_format.py b/tests/test_format.py index 8e0ada99cba..fb4d8eb4346 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -33,9 +33,10 @@ def check_file( @pytest.mark.parametrize("filename", all_data_cases("simple_cases")) def test_simple_format(filename: str) -> None: magic_trailing_comma = filename != "skip_magic_trailing_comma" - check_file( - "simple_cases", filename, black.Mode(magic_trailing_comma=magic_trailing_comma) + mode = black.Mode( + magic_trailing_comma=magic_trailing_comma, is_pyi=filename.endswith("_pyi") ) + check_file("simple_cases", filename, mode) @pytest.mark.parametrize("filename", all_data_cases("preview")) From b8e2ec728cc09d0f00829a9cffcb54da3efa5760 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:28:26 -0700 Subject: [PATCH 492/700] Fix crash on type comment with trailing space (#3773) --- CHANGES.md | 2 ++ src/black/parsing.py | 9 ++++++--- tests/data/simple_cases/comments2.py | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb304296d63..c7389ce57c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ under some circumstances. (#3745) - Fix a bug where multi-line open parenthesis magic comment like `type: ignore` were not correctly parsed (#3740) +- Fix error in AST validation when Black removes trailing whitespace in a type comment + (#3773) ### Preview style diff --git a/src/black/parsing.py b/src/black/parsing.py index 455c5eed968..e98e019cac6 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -208,15 +208,18 @@ def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: else: normalized: object - # Constant strings may be indented across newlines, if they are - # docstrings; fold spaces after newlines when comparing. Similarly, - # trailing and leading space may be removed. if ( isinstance(node, ast.Constant) and field == "value" and isinstance(value, str) ): + # Constant strings may be indented across newlines, if they are + # docstrings; fold spaces after newlines when comparing. Similarly, + # trailing and leading space may be removed. normalized = _normalize("\n", value) + elif field == "type_comment" and isinstance(value, str): + # Trailing whitespace in type comments is removed. + normalized = value.rstrip() else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" diff --git a/tests/data/simple_cases/comments2.py b/tests/data/simple_cases/comments2.py index 37e185abf4f..1487dc4b6e2 100644 --- a/tests/data/simple_cases/comments2.py +++ b/tests/data/simple_cases/comments2.py @@ -154,6 +154,9 @@ def _init_host(self, parsed) -> None: not parsed.hostname.strip()): pass + +a = "type comment with trailing space" # type: str + ####################### ### SECTION COMMENT ### ####################### @@ -332,6 +335,8 @@ def _init_host(self, parsed) -> None: pass +a = "type comment with trailing space" # type: str + ####################### ### SECTION COMMENT ### ####################### From ad3724b7ffc01d8152c97fe7f4dcf35220f21a8e Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 9 Jul 2023 17:04:50 -0700 Subject: [PATCH 493/700] Upgrade to latest mypy (#3775) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb647763e70..c2f4b1684e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - flake8-simplify - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.4.1 hooks: - id: mypy exclude: ^docs/conf.py From 138769aa27d6bd86507a0cd98d9a5bf8f63a8e99 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 10 Jul 2023 08:37:12 -0700 Subject: [PATCH 494/700] Disable coverage on pypy tests (#3777) The pypy tests are reeeeaaally slow. Maybe this will help. --- .github/workflows/test.yml | 4 +++- tox.ini | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92d7d411510..4bf687435b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,7 +58,9 @@ jobs: - name: Upload coverage to Coveralls # Upload coverage if we are on the main repository and # we're running on Linux (this action only supports Linux) - if: github.repository == 'psf/black' && matrix.os == 'ubuntu-latest' + if: + github.repository == 'psf/black' && matrix.os == 'ubuntu-latest' && + !startsWith(matrix.python-version, 'pypy') uses: AndreMiras/coveralls-python-action@v20201129 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/tox.ini b/tox.ini index f8e1a785331..d34dbbc71db 100644 --- a/tox.ini +++ b/tox.ini @@ -39,19 +39,15 @@ deps = ; remove this when pypy releases the bugfix commands = pip install -e .[d] - coverage erase pytest tests \ --run-optional no_jupyter \ !ci: --numprocesses auto \ - ci: --numprocesses 1 \ - --cov {posargs} + ci: --numprocesses 1 pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ !ci: --numprocesses auto \ - ci: --numprocesses 1 \ - --cov --cov-append {posargs} - coverage report + ci: --numprocesses 1 [testenv:{,ci-}311] setenv = From 38723bb7787d50f8751fad2eaa48b52c9e94c18d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:49:40 -0700 Subject: [PATCH 495/700] Unpin pytest-xdist (#3772) --- test_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_requirements.txt b/test_requirements.txt index ef61a1210ee..a3d262bc53d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,6 +1,6 @@ coverage >= 5.3 pre-commit pytest >= 6.1.1 -pytest-xdist >= 2.2.1, < 3.0.2 -pytest-cov >= 2.11.1 +pytest-xdist >= 3.0.2 +pytest-cov >= 4.1.0 tox From 193ee766ca496871f93621d6b58d57a6564ff81b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 10 Jul 2023 17:09:47 -0700 Subject: [PATCH 496/700] Prepare release 23.7.0 (#3776) --- CHANGES.md | 89 +++++++++++++-------- docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c7389ce57c6..c61ee698c5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,97 +6,118 @@ +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + +## 23.7.0 + +### Highlights + - Runtime support for Python 3.7 has been removed. Formatting 3.7 code will still be supported until further notice (#3765) ### Stable style - - - Fix a bug where an illegal trailing comma was added to return type annotations using PEP 604 unions (#3735) - Fix several bugs and crashes where comments in stub files were removed or mishandled - under some circumstances. (#3745) -- Fix a bug where multi-line open parenthesis magic comment like `type: ignore` were not - correctly parsed (#3740) -- Fix error in AST validation when Black removes trailing whitespace in a type comment + under some circumstances (#3745) +- Fix a crash with multi-line magic comments like `type: ignore` within parentheses + (#3740) +- Fix error in AST validation when _Black_ removes trailing whitespace in a type comment (#3773) ### Preview style - - - Implicitly concatenated strings used as function args are no longer wrapped inside parentheses (#3640) - Remove blank lines between a class definition and its docstring (#3692) ### Configuration - - -- The `--workers` argument to Black can now be specified via the `BLACK_NUM_WORKERS` +- The `--workers` argument to _Black_ can now be specified via the `BLACK_NUM_WORKERS` environment variable (#3743) - `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691) -- Fix black not honouring `pyproject.toml` settings when running `--stdin-filename` and - the `pyproject.toml` found isn't in the current working directory (#3719) -- Black will now error if `exclude` and `extend-exclude` have invalid data types in +- Fix _Black_ not honouring `pyproject.toml` settings when running `--stdin-filename` + and the `pyproject.toml` found isn't in the current working directory (#3719) +- _Black_ will now error if `exclude` and `extend-exclude` have invalid data types in `pyproject.toml`, instead of silently doing the wrong thing (#3764) ### Packaging - - - Upgrade mypyc from 0.991 to 1.3 (#3697) - Remove patching of Click that mitigated errors on Python 3.6 with `LANG=C` (#3768) ### Parser - - - Add support for the new PEP 695 syntax in Python 3.12 (#3703) ### Performance - - - Speed up _Black_ significantly when the cache is full (#3751) - Avoid importing `IPython` in a case where we wouldn't need it (#3748) ### Output - - - Use aware UTC datetimes internally, avoids deprecation warning on Python 3.12 (#3728) - Change verbose logging to exactly mirror _Black_'s logic for source discovery (#3749) ### _Blackd_ - - - The `blackd` argument parser now shows the default values for options in their help text (#3712) ### Integrations - - - Black is now tested with [`PYTHONWARNDEFAULTENCODING = 1`](https://docs.python.org/3/library/io.html#io-encoding-warning) (#3763) - Update GitHub Action to display black output in the job summary (#3688) -- Deprecated `set-output` command in CI test to keep up to date with GitHub's - deprecation announcement (#3757) ### Documentation - Add a CITATION.cff file to the root of the repository, containing metadata on how to cite this software (#3723) - - - -- Updated the _classes_ and _exceptions_ documentation in Developer reference to match - the latest ccode base. (#3755) +- Update the _classes_ and _exceptions_ documentation in Developer reference to match + the latest code base (#3755) ## 23.3.0 diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 8b8fd658e0e..a9d33d2d853 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: 23.3.0 + rev: 23.7.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -31,7 +31,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac ```yaml repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black-jupyter # 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 2a461487210..f5862edccaa 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -193,8 +193,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.3.0 (compiled: yes) -$ black --required-version 23.3.0 -c "format = 'this'" +black, 23.7.0 (compiled: yes) +$ black --required-version 23.7.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -285,7 +285,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.3.0 +black, 23.7.0 ``` #### `--config` From a062d5c9854205c4ae3f4cbe8859ed59bcd6259c Mon Sep 17 00:00:00 2001 From: skykasko <88055150+skykasko@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:38:01 -0400 Subject: [PATCH 497/700] Fix typo in CITATION.cff (#3779) Fix tiny typo in CITATION.cff --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index ddf64f616ff..7ff0e3ca9bc 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -11,7 +11,7 @@ repository-code: "https://github.com/psf/black" url: "https://black.readthedocs.io/en/stable/" abstract: >- Black is the uncompromising Python code formatter. By using it, you agree to cede - control over minutiae ofhand-formatting. In return, Black gives you speed, + control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters. From 027afda403d5da7b0ea2a1bf40788ad4c3eb510e Mon Sep 17 00:00:00 2001 From: Nicola Soranzo Date: Tue, 11 Jul 2023 15:21:15 +0100 Subject: [PATCH 498/700] Remove Python 3.7 from classifiers (#3784) Follow-up on https://github.com/psf/black/pull/3765 --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 175f7851dee..aaac42b44b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From f4490acfd7bf466ae30d7573d85107b46d4686b3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 11 Jul 2023 15:21:36 +0100 Subject: [PATCH 499/700] Remove unneeded mypy dependencies (#3783) --- .pre-commit-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2f4b1684e6..3561df4f904 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,10 +43,8 @@ repos: - id: mypy exclude: ^docs/conf.py additional_dependencies: - - types-dataclasses >= 0.1.3 - types-PyYAML - tomli >= 0.2.6, < 2.0.0 - - types-typed-ast >= 1.4.1 - click >= 8.1.0, != 8.1.4 - packaging >= 22.0 - platformdirs >= 2.1.0 From 8d2110320bef37534997af47edff243d1ec2720b Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 11 Jul 2023 07:35:41 -0700 Subject: [PATCH 500/700] Fix lint in test_ipynb (#3781) Unblocks #3780 --- tests/test_ipynb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index 91e7901125b..a74f8ad5690 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -77,7 +77,7 @@ def test_trailing_semicolon_noop() -> None: [ pytest.param(JUPYTER_MODE, id="default mode"), pytest.param( - replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}), id="custom cell magics mode", ), ], @@ -100,7 +100,7 @@ def test_cell_magic_noop() -> None: [ pytest.param(JUPYTER_MODE, id="default mode"), pytest.param( - replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}), id="custom cell magics mode", ), ], @@ -183,7 +183,7 @@ def test_cell_magic_with_magic() -> None: id="No change when cell magic not registered", ), pytest.param( - replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}), + replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}), "%%custom_python_magic -n1 -n2\nx=2", pytest.raises(NothingChanged), id="No change when other cell magics registered", From 37895f8e50486a0fa581f8fb039e536dc6d0d0e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 07:50:52 -0700 Subject: [PATCH 501/700] [pre-commit.ci] pre-commit autoupdate (#3780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/flake8: 4.0.1 → 6.0.0](https://github.com/pycqa/flake8/compare/4.0.1...6.0.0) - [github.com/pre-commit/mirrors-prettier: v2.7.1 → v3.0.0](https://github.com/pre-commit/mirrors-prettier/compare/v2.7.1...v3.0.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3561df4f904..70b4cd82532 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: isort - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: @@ -52,13 +52,13 @@ repos: - hypothesis - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.7.1 + rev: v3.0.0 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace From 6123b4ac2696116090fee3da77e9be66417980dd Mon Sep 17 00:00:00 2001 From: rax <133822160+kotnen@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:16:43 -0500 Subject: [PATCH 502/700] Document shebang comment behaviour (#3787) --- docs/the_black_code_style/current_style.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 0fb59fe5aae..e1a8078bf2c 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -254,11 +254,12 @@ required due to an inner function starting immediately after. _Black_ does not format comment contents, but it enforces two spaces between code and a comment on the same line, and a space before the comment text begins. Some types of -comments that require specific spacing rules are respected: doc comments (`#: comment`), -section comments with long runs of hashes, and Spyder cells. Non-breaking spaces after -hashes are also preserved. Comments may sometimes be moved because of formatting -changes, which can break tools that assign special meaning to them. See -[AST before and after formatting](#ast-before-and-after-formatting) for more discussion. +comments that require specific spacing rules are respected: shebangs (`#! comment`), doc +comments (`#: comment`), section comments with long runs of hashes, and Spyder cells. +Non-breaking spaces after hashes are also preserved. Comments may sometimes be moved +because of formatting changes, which can break tools that assign special meaning to +them. See [AST before and after formatting](#ast-before-and-after-formatting) for more +discussion. ### Trailing commas From 0e26ada66d16d2aea97bda5f907bb0b20b0985e7 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 16 Jul 2023 17:35:19 -0700 Subject: [PATCH 503/700] Continue to avoid Click typing issue (#3791) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70b4cd82532..89c0de39c86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: additional_dependencies: - types-PyYAML - tomli >= 0.2.6, < 2.0.0 - - click >= 8.1.0, != 8.1.4 + - click >= 8.1.0, != 8.1.4, != 8.1.5 - packaging >= 22.0 - platformdirs >= 2.1.0 - pytest From 92e0f5b96500459b232a927fb26b0c990800b586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 17 Jul 2023 03:09:26 +0200 Subject: [PATCH 504/700] Avoid importing `IPython` if notebook cells do not contain magics (#3782) Co-authored-by: hauntsaninja Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- CHANGES.md | 2 ++ src/black/__init__.py | 2 +- src/black/files.py | 2 +- src/black/handle_ipynb_magics.py | 31 ++++++++++++------------------- tests/test_ipynb.py | 12 ++++-------- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c61ee698c5d..709c767b329 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,8 @@ +- Avoid importing `IPython` if notebook cells do not contain magics (#3782) + ### Output diff --git a/src/black/__init__.py b/src/black/__init__.py index 301c18f7338..923a51867b5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -668,7 +668,7 @@ def get_sources( p = Path(f"{STDIN_PLACEHOLDER}{str(p)}") if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed( - verbose=verbose, quiet=quiet + warn=verbose or not quiet ): continue diff --git a/src/black/files.py b/src/black/files.py index ef6895ee3af..368e4170d47 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -384,7 +384,7 @@ def gen_python_files( elif child.is_file(): if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed( - verbose=verbose, quiet=quiet + warn=verbose or not quiet ): continue include_match = include.search(normalized_path) if include else True diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 2a2d62220e2..55ef2267df8 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -6,6 +6,7 @@ import secrets import sys from functools import lru_cache +from importlib.util import find_spec from typing import Dict, List, Optional, Tuple if sys.version_info >= (3, 10): @@ -56,25 +57,17 @@ class Replacement: @lru_cache -def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: - try: - # isort: off - # tokenize_rt is less commonly installed than IPython - # and IPython is expensive to import - import tokenize_rt # noqa:F401 - import IPython # noqa:F401 - - # isort: on - except ModuleNotFoundError: - if verbose or not quiet: - msg = ( - "Skipping .ipynb files as Jupyter dependencies are not installed.\n" - 'You can fix this by running ``pip install "black[jupyter]"``' - ) - out(msg) - return False - else: - return True +def jupyter_dependencies_are_installed(*, warn: bool) -> bool: + installed = ( + find_spec("tokenize_rt") is not None and find_spec("IPython") is not None + ) + if not installed and warn: + msg = ( + "Skipping .ipynb files as Jupyter dependencies are not installed.\n" + 'You can fix this by running ``pip install "black[jupyter]"``' + ) + out(msg) + return installed def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py index a74f8ad5690..59897190304 100644 --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -440,17 +440,13 @@ def test_cache_isnt_written_if_no_jupyter_deps_single( nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb") tmp_nb = tmp_path / "notebook.ipynb" tmp_nb.write_bytes(nb.read_bytes()) - monkeypatch.setattr( - "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False - ) + monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: False) 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 - ) + monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: True) result = runner.invoke( main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"] ) @@ -466,13 +462,13 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir( tmp_nb = tmp_path / "notebook.ipynb" tmp_nb.write_bytes(nb.read_bytes()) monkeypatch.setattr( - "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False + "black.files.jupyter_dependencies_are_installed", lambda warn: False ) 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 + "black.files.jupyter_dependencies_are_installed", lambda warn: True ) result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"]) assert "reformatted" in result.output From 8d80aecd50ea55a817807ae2d5174ccedaf12ecb Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sun, 16 Jul 2023 21:16:12 -0400 Subject: [PATCH 505/700] Maintainers += Shantanu Jain (hauntsaninja) (#3792) --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index ab3f30b8821..e0511bb9b7c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -13,6 +13,7 @@ Maintained with: - [Richard Si](mailto:sichard26@gmail.com) - [Felix Hildén](mailto:felix.hilden@gmail.com) - [Batuhan Taskaya](mailto:batuhan@python.org) +- [Shantanu Jain](mailto:hauntsaninja@gmail.com) Multiple contributions by: From c1e30d97fe39e4c1b1967571b7e3854547239bf6 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 16 Jul 2023 21:33:58 -0700 Subject: [PATCH 506/700] Fix most blib2to3 lint (#3794) --- .pre-commit-config.yaml | 3 ++- pyproject.toml | 5 ++--- src/blib2to3/README | 13 +++++++------ src/blib2to3/pgen2/driver.py | 23 +++++++---------------- src/blib2to3/pgen2/literals.py | 2 -- src/blib2to3/pgen2/parse.py | 27 +++++++++++++++------------ src/blib2to3/pgen2/pgen.py | 25 +++++++++++-------------- src/blib2to3/pgen2/token.py | 4 +--- src/blib2to3/pgen2/tokenize.py | 29 ++++++++++++++++++++--------- src/blib2to3/pygram.py | 2 -- src/blib2to3/pytree.py | 11 +++-------- 11 files changed, 68 insertions(+), 76 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89c0de39c86..0d68b81ccd7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ # Note: don't use this config for your own repositories. Instead, see # "Version control integration" in docs/integrations/source_version_control.md -exclude: ^(src/blib2to3/|profiling/|tests/data/) +exclude: ^(profiling/|tests/data/) repos: - repo: local hooks: @@ -36,6 +36,7 @@ repos: - flake8-bugbear - flake8-comprehensions - flake8-simplify + exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.4.1 diff --git a/pyproject.toml b/pyproject.toml index aaac42b44b9..d29b768c289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,7 @@ include = '\.pyi?$' extend-exclude = ''' /( # The following are specific to Black, you probably don't want those. - | blib2to3 - | tests/data + tests/data | profiling )/ ''' @@ -183,7 +182,7 @@ atomic = true profile = "black" line_length = 88 skip_gitignore = true -skip_glob = ["src/blib2to3", "tests/data", "profiling"] +skip_glob = ["tests/data", "profiling"] known_first_party = ["black", "blib2to3", "blackd", "_black_version"] [tool.pytest.ini_options] diff --git a/src/blib2to3/README b/src/blib2to3/README index 0d3c607c9c7..38b04158ddb 100644 --- a/src/blib2to3/README +++ b/src/blib2to3/README @@ -1,18 +1,19 @@ -A subset of lib2to3 taken from Python 3.7.0b2. -Commit hash: 9c17e3a1987004b8bcfbe423953aad84493a7984 +A subset of lib2to3 taken from Python 3.7.0b2. Commit hash: +9c17e3a1987004b8bcfbe423953aad84493a7984 Reasons for forking: + - consistent handling of f-strings for users of Python < 3.6.2 -- backport of BPO-33064 that fixes parsing files with trailing commas after - *args and **kwargs -- backport of GH-6143 that restores the ability to reformat legacy usage of - `async` +- backport of BPO-33064 that fixes parsing files with trailing commas after \*args and + \*\*kwargs +- backport of GH-6143 that restores the ability to reformat legacy usage of `async` - support all types of string literals - better ability to debug (better reprs) - INDENT and DEDENT don't hold whitespace and comment prefixes - ability to Cythonize Change Log: + - Changes default logger used by Driver - Backported the following upstream parser changes: - "bpo-42381: Allow walrus in set literals and set comprehensions (GH-23332)" diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index bb73016a4c1..e629843f8b9 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -17,30 +17,21 @@ # Python imports import io -import os import logging +import os import pkgutil import sys -from typing import ( - Any, - cast, - IO, - Iterable, - List, - Optional, - Iterator, - Tuple, - Union, -) from contextlib import contextmanager from dataclasses import dataclass, field - -# Pgen imports -from . import grammar, parse, token, tokenize, pgen from logging import Logger -from blib2to3.pytree import NL +from typing import IO, Any, Iterable, Iterator, List, Optional, Tuple, Union, cast + from blib2to3.pgen2.grammar import Grammar from blib2to3.pgen2.tokenize import GoodTokenInfo +from blib2to3.pytree import NL + +# Pgen imports +from . import grammar, parse, pgen, token, tokenize Path = Union[str, "os.PathLike[str]"] diff --git a/src/blib2to3/pgen2/literals.py b/src/blib2to3/pgen2/literals.py index c67b91d0463..53c0b8ac2bb 100644 --- a/src/blib2to3/pgen2/literals.py +++ b/src/blib2to3/pgen2/literals.py @@ -4,10 +4,8 @@ """Safely evaluate Python string literals without using eval().""" import re - from typing import Dict, Match - simple_escapes: Dict[str, str] = { "a": "\a", "b": "\b", diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 17bf118e9fc..299cc24a15f 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -10,24 +10,25 @@ """ from contextlib import contextmanager - -# Local imports -from . import grammar, token, tokenize from typing import ( - cast, + TYPE_CHECKING, Any, - Optional, - Union, - Tuple, + Callable, Dict, - List, Iterator, - Callable, + List, + Optional, Set, - TYPE_CHECKING, + Tuple, + Union, + cast, ) + from blib2to3.pgen2.grammar import Grammar -from blib2to3.pytree import convert, NL, Context, RawNode, Leaf, Node +from blib2to3.pytree import NL, Context, Leaf, Node, RawNode, convert + +# Local imports +from . import grammar, token, tokenize if TYPE_CHECKING: from blib2to3.pgen2.driver import TokenProxy @@ -112,7 +113,9 @@ def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None: args.insert(0, ilabel) func(*args) - def determine_route(self, value: Optional[str] = None, force: bool = False) -> Optional[int]: + def determine_route( + self, value: Optional[str] = None, force: bool = False + ) -> Optional[int]: alive_ilabels = self.ilabels if len(alive_ilabels) == 0: *_, most_successful_ilabel = self._dead_ilabels diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index 046efd09338..3ece9bb41ed 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -1,25 +1,22 @@ # Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved. # Licensed to PSF under a Contributor Agreement. -# Pgen imports -from . import grammar, token, tokenize - +import os from typing import ( + IO, Any, Dict, - IO, Iterator, List, + NoReturn, Optional, + Sequence, Tuple, Union, - Sequence, - NoReturn, ) -from blib2to3.pgen2 import grammar -from blib2to3.pgen2.tokenize import GoodTokenInfo -import os +from blib2to3.pgen2 import grammar, token, tokenize +from blib2to3.pgen2.tokenize import GoodTokenInfo Path = Union[str, "os.PathLike[str]"] @@ -149,7 +146,7 @@ def calcfirst(self, name: str) -> None: state = dfa[0] totalset: Dict[str, int] = {} overlapcheck = {} - for label, next in state.arcs.items(): + for label in state.arcs: if label in self.dfas: if label in self.first: fset = self.first[label] @@ -190,9 +187,9 @@ def parse(self) -> Tuple[Dict[str, List["DFAState"]], str]: # self.dump_nfa(name, a, z) dfa = self.make_dfa(a, z) # self.dump_dfa(name, dfa) - oldlen = len(dfa) + # oldlen = len(dfa) self.simplify_dfa(dfa) - newlen = len(dfa) + # newlen = len(dfa) dfas[name] = dfa # print name, oldlen, newlen if startsymbol is None: @@ -346,7 +343,7 @@ def parse_atom(self) -> Tuple["NFAState", "NFAState"]: self.raise_error( "expected (...) or NAME or STRING, got %s/%s", self.type, self.value ) - assert False + raise AssertionError def expect(self, type: int, value: Optional[Any] = None) -> str: if self.type != type or (value is not None and self.value != value): @@ -368,7 +365,7 @@ def raise_error(self, msg: str, *args: Any) -> NoReturn: if args: try: msg = msg % args - except: + except Exception: msg = " ".join([msg] + list(map(str, args))) raise SyntaxError(msg, (self.filename, self.end[0], self.end[1], self.line)) diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index 117cc09d4ce..ed2fc4e85fc 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -1,8 +1,6 @@ """Token constants (from "token.h").""" -from typing import Dict - -from typing import Final +from typing import Dict, Final # Taken from Python (r53757) and modified to include some tokens # originally monkeypatched in by pgen2.tokenize diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 1dea89d7bb8..d0607f4b1e1 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -30,28 +30,41 @@ import sys from typing import ( Callable, + Final, Iterable, Iterator, List, Optional, + Pattern, Set, Tuple, - Pattern, Union, cast, ) -from typing import Final - -from blib2to3.pgen2.token import * from blib2to3.pgen2.grammar import Grammar +from blib2to3.pgen2.token import ( + ASYNC, + AWAIT, + COMMENT, + DEDENT, + ENDMARKER, + ERRORTOKEN, + INDENT, + NAME, + NEWLINE, + NL, + NUMBER, + OP, + STRING, + tok_name, +) __author__ = "Ka-Ping Yee " __credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro" import re from codecs import BOM_UTF8, lookup -from blib2to3.pgen2.token import * from . import token @@ -334,7 +347,7 @@ def read_or_stop() -> bytes: try: return readline() except StopIteration: - return b'' + return b"" def find_cookie(line: bytes) -> Optional[str]: try: @@ -676,14 +689,12 @@ def generate_tokens( yield stashed stashed = None - for indent in indents[1:]: # pop remaining indent levels + for _indent in indents[1:]: # pop remaining indent levels yield (DEDENT, "", (lnum, 0), (lnum, 0), "") yield (ENDMARKER, "", (lnum, 0), (lnum, 0), "") if __name__ == "__main__": # testing - import sys - if len(sys.argv) > 1: tokenize(open(sys.argv[1]).readline) else: diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index 1b4832362bf..c30c630e816 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -5,12 +5,10 @@ # Python imports import os - from typing import Union # Local imports from .pgen2 import driver - from .pgen2.grammar import Grammar # Moved into initialize because mypyc can't handle __file__ (XXX bug) diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 156322cab7e..2a0cd6d196a 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -15,15 +15,16 @@ from typing import ( Any, Dict, + Iterable, Iterator, List, Optional, + Set, Tuple, TypeVar, Union, - Set, - Iterable, ) + from blib2to3.pgen2.grammar import Grammar __author__ = "Guido van Rossum " @@ -58,7 +59,6 @@ def type_repr(type_num: int) -> Union[str, int]: class Base: - """ Abstract base class for Node and Leaf. @@ -237,7 +237,6 @@ def get_suffix(self) -> str: class Node(Base): - """Concrete implementation for interior nodes.""" fixers_applied: Optional[List[Any]] @@ -378,7 +377,6 @@ def update_sibling_maps(self) -> None: class Leaf(Base): - """Concrete implementation for leaf nodes.""" # Default values for instance variables @@ -506,7 +504,6 @@ def convert(gr: Grammar, raw_node: RawNode) -> NL: class BasePattern: - """ A pattern is a tree matching pattern. @@ -646,7 +643,6 @@ def _submatch(self, node, results=None): class NodePattern(BasePattern): - wildcards: bool = False def __init__( @@ -715,7 +711,6 @@ def _submatch(self, node, results=None) -> bool: class WildcardPattern(BasePattern): - """ A wildcard pattern can match zero or more nodes. From 068f6fb8fa2b52f647aec8696033e43f6b0db70b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:59:36 -0700 Subject: [PATCH 507/700] Bump pypa/cibuildwheel from 2.13.1 to 2.14.1 (#3795) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 06600fcbc45..291193efc7a 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.13.1 + uses: pypa/cibuildwheel@v2.14.1 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 2f68ac850b5b5b8e955110112f841121b76effa4 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Tue, 18 Jul 2023 10:51:16 -0400 Subject: [PATCH 508/700] Fix diff-shades comment missing newlines (#3799) Preserving newlines is done differently when writing to $GITHUB_OUTPUT over the deprecated :set-output: command. --- scripts/diff_shades_gha_helper.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 994fbe05045..7a58fbe9b28 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -21,6 +21,7 @@ import subprocess import sys import zipfile +from base64 import b64encode from io import BytesIO from pathlib import Path from typing import Any @@ -53,12 +54,16 @@ def set_output(name: str, value: str) -> None: else: print(f"[INFO]: setting '{name}' to [{len(value)} chars]") - # Originally the `set-output` workflow command was used here, now replaced - # by setting variables through the `GITHUB_OUTPUT` environment variable - # to stay up to date with GitHub's update. if "GITHUB_OUTPUT" in os.environ: + if "\n" in value: + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings + delimiter = b64encode(os.urandom(16)).decode() + value = f"{delimiter}\n{value}\n{delimiter}" + command = f"{name}<<{value}" + else: + command = f"{name}={value}" with open(os.environ["GITHUB_OUTPUT"], "a") as f: - print(f"{name}={value}", file=f) + print(command, file=f) def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any: @@ -224,9 +229,7 @@ def comment_details(run_id: str) -> None: # while it's still in progress seems impossible). body = body.replace("$workflow-run-url", data["html_url"]) body = body.replace("$job-diff-url", diff_url) - # https://github.community/t/set-output-truncates-multiline-strings/16852/3 - escaped = body.replace("%", "%25").replace("\n", "%0A").replace("\r", "%0D") - set_output("comment-body", escaped) + set_output("comment-body", body) if __name__ == "__main__": From 0b301f80954a026693c4c22de89267ad8c85f9b6 Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Tue, 18 Jul 2023 14:11:24 -0700 Subject: [PATCH 509/700] Improvements to contributing docs (#3753) --- docs/contributing/the_basics.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 5fdcdd802bd..40d233257e3 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -12,7 +12,9 @@ example: ```console $ python3 -m venv .venv -$ source .venv/bin/activate +$ source .venv/bin/activate # activation for linux and mac +$ .venv\Scripts\activate # activation for windows + (.venv)$ pip install -r test_requirements.txt (.venv)$ pip install -e .[d] (.venv)$ pre-commit install @@ -30,6 +32,9 @@ the root of the black repo: # Optional Fuzz testing (.venv)$ tox -e fuzz + +# Format Black itself +(.venv)$ tox -e run_self ``` ### News / Changelog Requirement @@ -62,7 +67,7 @@ If you make changes to docs, you can test they still build locally too. ```console (.venv)$ pip install -r docs/requirements.txt -(.venv)$ pip install [-e] .[d] +(.venv)$ pip install -e .[d] (.venv)$ sphinx-build -a -b html -W docs/ docs/_build/ ``` From e7e8d6287b38db3f15bdf3d4ec6987d4490b8d14 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 22 Jul 2023 08:49:51 -0700 Subject: [PATCH 510/700] Simplify empty line tracker (#3797) --- src/black/lines.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index ea8fe520756..016a489310d 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -49,7 +49,7 @@ class Line: """Holds leaves and comments. Can be printed with `str(line)`.""" - mode: Mode + mode: Mode = field(repr=False) depth: int = 0 leaves: List[Leaf] = field(default_factory=list) # keys ordered like `leaves` @@ -579,16 +579,21 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: else: before = 0 depth = current_line.depth + + previous_def = None while self.previous_defs and self.previous_defs[-1].depth >= depth: + previous_def = self.previous_defs.pop() + + if previous_def is not None: + assert self.previous_line is not None if self.mode.is_pyi: - assert self.previous_line is not None if depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. before = min(1, before) elif ( Preview.blank_line_after_nested_stub_class in self.mode - and self.previous_defs[-1].is_class - and not self.previous_defs[-1].is_stub_class + and previous_def.is_class + and not previous_def.is_stub_class ): before = 1 elif depth: @@ -600,7 +605,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 1 elif ( not depth - and self.previous_defs[-1].depth + and previous_def.depth and current_line.leaves[-1].type == token.COLON and ( current_line.leaves[0].value @@ -617,7 +622,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 1 else: before = 2 - self.previous_defs.pop() + if current_line.is_decorator or current_line.is_def or current_line.is_class: return self._maybe_empty_lines_for_class_or_def(current_line, before) From 13bd4fffae0b95b0a1f55d335dd55a1de7de3d10 Mon Sep 17 00:00:00 2001 From: mihazagar Date: Sat, 22 Jul 2023 20:12:37 +0200 Subject: [PATCH 511/700] Fixing pre-commit using pyyaml with broken version (#3804) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d68b81ccd7..10e65316e82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: additional_dependencies: &version_check_dependencies [ commonmark==0.9.1, - pyyaml==5.4.1, + pyyaml==6.0.1, beautifulsoup4==4.9.3, ] From c3235e6da7259394cd0c00fe36c3e089fbae1e4f Mon Sep 17 00:00:00 2001 From: Pradeep Kumar Date: Sun, 23 Jul 2023 21:56:19 -0700 Subject: [PATCH 512/700] Fix unintentionally swapped words in index.md (#3809) Fix unintentionally swapped words in index.md I think the intent was to say "large changes in formatting", because it doesn't make sense to say "large formatting in changes". --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 9d0db465022..49a44ecca5a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). *Black* is [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, our own auto formatting and parallel Continuous Integration runner. -Now that we have become stable, you should not expect large formatting to changes in +Now that we have become stable, you should not expect large changes to formatting in the future. Stylistic changes will mostly be responses to bug reports and support for new Python syntax. From d9d0a02d89207f712a40b6dabee708389208e558 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Thu, 27 Jul 2023 16:12:38 +0200 Subject: [PATCH 513/700] Fix typo in `target-version` param wrongly used in plural (#3817) --- docs/usage_and_configuration/the_basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index f5862edccaa..5efb50a9a12 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -63,7 +63,7 @@ $ black -t py37 -t py38 -t py39 -t py310 In a [configuration file](#configuration-via-a-file), you can write: ```toml -target-versions = ["py37", "py38", "py39", "py310"] +target-version = ["py37", "py38", "py39", "py310"] ``` _Black_ uses this option to decide what grammar to use to parse your code. In addition, From 133af572072bf7bc92c23a609773c2ea66e483b7 Mon Sep 17 00:00:00 2001 From: freddiewanah Date: Fri, 28 Jul 2023 02:51:28 +1000 Subject: [PATCH 514/700] Rewrite mostly useless assert in test_trans.py (#3810) This PR updates an assert statement that checks the bounds of a string-slicing operation. The updated assertion provides more accurate and informative error handling by specifically checking the relative values of the indices and the string length. The original assertion was essentially checking if Python's string slicing was behaving as expected. However, it wasn't providing any guarantees or useful information about the bounds i and j themselves. The updated assertion checks that the indices used for slicing are within the bounds of the string. It will throw an AssertionError if the indices are out of bounds or if i > j, providing a more specific and informative error. --- tests/test_trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trans.py b/tests/test_trans.py index dce8a939677..784e852e12a 100644 --- a/tests/test_trans.py +++ b/tests/test_trans.py @@ -13,7 +13,7 @@ def check( # a glance than only spans assert len(spans) == len(expected_slices) for (i, j), slice in zip(spans, expected_slices): - assert len(string[i:j]) == j - i + assert 0 <= i <= j <= len(string) assert string[i:j] == slice assert spans == expected_spans From 1a972e3e11b144912155babdf48ff23d68059d57 Mon Sep 17 00:00:00 2001 From: Aneesh Agrawal Date: Thu, 27 Jul 2023 17:50:51 -0400 Subject: [PATCH 515/700] Add Lyft to organizations using black (#3818) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b12ddfb1290..9d0b29af215 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,8 @@ SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtuale 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, Archer Aviation. +The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Lyft, Mozilla, +Quora, Duolingo, QuantumBlack, Tesla, Archer Aviation. Are we missing anyone? Let us know. From 8a16b25fb1145e5b7de9c322e52167e8f6a59c79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 08:43:32 -0700 Subject: [PATCH 516/700] Bump furo from 2023.5.20 to 2023.7.26 in /docs (#3824) Bumps [furo](https://github.com/pradyunsg/furo) from 2023.5.20 to 2023.7.26. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.05.20...2023.07.26) --- updated-dependencies: - dependency-name: furo 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 f1b47c69413..ff179f3805e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==6.1.3 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.2 -furo==2023.5.20 +furo==2023.7.26 From 1b028cc9d99c2c2e82f9b727742539173a92a373 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 01:48:21 -0700 Subject: [PATCH 517/700] [pre-commit.ci] pre-commit autoupdate (#3825) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10e65316e82..5430eef9180 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: isort - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: From 59e8936768889f583488df609c45302da8e88507 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:46:08 -0700 Subject: [PATCH 518/700] Document pre-commit mirror (#3828) --- .pre-commit-hooks.yaml | 2 ++ CHANGES.md | 5 +++++ docs/integrations/source_version_control.md | 13 ++++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 137957045a6..a1ff41fded8 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,3 +1,5 @@ +# Note that we recommend using https://github.com/psf/black-pre-commit-mirror instead +# This will work about 2x as fast as using the hooks in this repository - id: black name: black description: "Black: The uncompromising Python code formatter" diff --git a/CHANGES.md b/CHANGES.md index 709c767b329..1de08792f75 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,11 @@ +- Black now has an + [official pre-commit mirror](https://github.com/psf/black-pre-commit-mirror). Swapping + `https://github.com/psf/black` to `https://github.com/psf/black-pre-commit-mirror` in + your `.pre-commit-config.yaml` will make Black about 2x faster (#3828) + ### Documentation +- More concise formatting for dummy implementations (#3796) + ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 5ef3bbd1705..507e860190f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -281,7 +281,9 @@ def visit_match_case(self, node: Node) -> Iterator[Line]: def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" - if self.mode.is_pyi and is_stub_suite(node): + if ( + self.mode.is_pyi or Preview.dummy_implementations in self.mode + ) and is_stub_suite(node): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -296,7 +298,9 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: - if self.mode.is_pyi and is_stub_body(node): + if ( + self.mode.is_pyi or Preview.dummy_implementations in self.mode + ) and is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) @@ -305,7 +309,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: else: if ( - not self.mode.is_pyi + not (self.mode.is_pyi or Preview.dummy_implementations in self.mode) or not node.parent or not is_stub_suite(node.parent) ): diff --git a/src/black/lines.py b/src/black/lines.py index 016a489310d..0a307b45eff 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -165,6 +165,13 @@ def is_def(self) -> bool: and second_leaf.value == "def" ) + @property + def is_stub_def(self) -> bool: + """Is this line a function definition with a body consisting only of "..."?""" + return self.is_def and self.leaves[-4:] == [Leaf(token.COLON, ":")] + [ + Leaf(token.DOT, ".") for _ in range(3) + ] + @property def is_class_paren_empty(self) -> bool: """Is this a class with no base classes but using parentheses? @@ -578,6 +585,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: first_leaf.prefix = "" else: before = 0 + + user_had_newline = bool(before) depth = current_line.depth previous_def = None @@ -589,7 +598,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if self.mode.is_pyi: if depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. - before = min(1, before) + before = 1 if user_had_newline else 0 elif ( Preview.blank_line_after_nested_stub_class in self.mode and previous_def.is_class @@ -624,7 +633,9 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: before = 2 if current_line.is_decorator or current_line.is_def or current_line.is_class: - return self._maybe_empty_lines_for_class_or_def(current_line, before) + return self._maybe_empty_lines_for_class_or_def( + current_line, before, user_had_newline + ) if ( self.previous_line @@ -648,8 +659,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 0 return before, 0 - def _maybe_empty_lines_for_class_or_def( - self, current_line: Line, before: int + def _maybe_empty_lines_for_class_or_def( # noqa: C901 + self, current_line: Line, before: int, user_had_newline: bool ) -> Tuple[int, int]: if not current_line.is_decorator: self.previous_defs.append(current_line) @@ -715,6 +726,14 @@ def _maybe_empty_lines_for_class_or_def( newlines = 0 else: newlines = 1 if current_line.depth else 2 + # If a user has left no space after a dummy implementation, don't insert + # new lines. This is useful for instance for @overload or Protocols. + if ( + Preview.dummy_implementations in self.mode + and self.previous_line.is_stub_def + and not user_had_newline + ): + newlines = 0 if comment_to_add_newlines is not None: previous_block = comment_to_add_newlines.previous_block if previous_block is not None: diff --git a/src/black/mode.py b/src/black/mode.py index 4d979afd84d..282c1669da7 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -182,6 +182,7 @@ class Preview(Enum): skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() + dummy_implementations = auto() class Deprecated(UserWarning): diff --git a/tests/data/miscellaneous/force_py36.py b/tests/data/miscellaneous/force_py36.py index cad935e525a..4c9b70336e7 100644 --- a/tests/data/miscellaneous/force_py36.py +++ b/tests/data/miscellaneous/force_py36.py @@ -1,6 +1,6 @@ # The input source must not contain any Py36-specific syntax (e.g. argument type # annotations, trailing comma after *rest) or this test becomes invalid. -def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ... +def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): pass # output # The input source must not contain any Py36-specific syntax (e.g. argument type # annotations, trailing comma after *rest) or this test becomes invalid. @@ -13,4 +13,4 @@ def long_function_name( argument_six, *rest, ): - ... + pass diff --git a/tests/data/preview/comments7.py b/tests/data/preview/comments7.py index ec2dc501d8e..8b1224017e5 100644 --- a/tests/data/preview/comments7.py +++ b/tests/data/preview/comments7.py @@ -278,8 +278,7 @@ class C: ) def test_fails_invalid_post_data( self, pyramid_config, db_request, post_data, message - ): - ... + ): ... square = Square(4) # type: Optional[Square] diff --git a/tests/data/preview/dummy_implementations.py b/tests/data/preview/dummy_implementations.py new file mode 100644 index 00000000000..e07c25ed129 --- /dev/null +++ b/tests/data/preview/dummy_implementations.py @@ -0,0 +1,99 @@ +from typing import NoReturn, Protocol, Union, overload + + +def dummy(a): ... +def other(b): ... + + +@overload +def a(arg: int) -> int: ... +@overload +def a(arg: str) -> str: ... +@overload +def a(arg: object) -> NoReturn: ... +def a(arg: Union[int, str, object]) -> Union[int, str]: + if not isinstance(arg, (int, str)): + raise TypeError + return arg + +class Proto(Protocol): + def foo(self, a: int) -> int: + ... + + def bar(self, b: str) -> str: ... + def baz(self, c: bytes) -> str: + ... + + +def dummy_two(): + ... +@dummy +def dummy_three(): + ... + +def dummy_four(): + ... + +@overload +def b(arg: int) -> int: ... + +@overload +def b(arg: str) -> str: ... +@overload +def b(arg: object) -> NoReturn: ... + +def b(arg: Union[int, str, object]) -> Union[int, str]: + if not isinstance(arg, (int, str)): + raise TypeError + return arg + +# output + +from typing import NoReturn, Protocol, Union, overload + + +def dummy(a): ... +def other(b): ... + + +@overload +def a(arg: int) -> int: ... +@overload +def a(arg: str) -> str: ... +@overload +def a(arg: object) -> NoReturn: ... +def a(arg: Union[int, str, object]) -> Union[int, str]: + if not isinstance(arg, (int, str)): + raise TypeError + return arg + + +class Proto(Protocol): + def foo(self, a: int) -> int: ... + + def bar(self, b: str) -> str: ... + def baz(self, c: bytes) -> str: ... + + +def dummy_two(): ... +@dummy +def dummy_three(): ... + + +def dummy_four(): ... + + +@overload +def b(arg: int) -> int: ... + + +@overload +def b(arg: str) -> str: ... +@overload +def b(arg: object) -> NoReturn: ... + + +def b(arg: Union[int, str, object]) -> Union[int, str]: + if not isinstance(arg, (int, str)): + raise TypeError + return arg From 77f19944f632c48765175cafad07dc76b3810911 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 08:41:39 -0700 Subject: [PATCH 520/700] [pre-commit.ci] pre-commit autoupdate (#3833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-prettier: v3.0.0 → v3.0.1](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0...v3.0.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5430eef9180..60a092f8b29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: - hypothesis - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0 + rev: v3.0.1 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml From c36e468794f9256d5e922c399240d49782ba04f1 Mon Sep 17 00:00:00 2001 From: Christian Proud Date: Wed, 9 Aug 2023 02:12:05 +0800 Subject: [PATCH 521/700] Remove ENV_PATH on Black action completion (#3759) --- CHANGES.md | 2 ++ action/main.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d084498f404..8bf4188606c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,8 @@ [official pre-commit mirror](https://github.com/psf/black-pre-commit-mirror). Swapping `https://github.com/psf/black` to `https://github.com/psf/black-pre-commit-mirror` in your `.pre-commit-config.yaml` will make Black about 2x faster (#3828) +- The `.black.env` folder specified by `ENV_PATH` will now be removed on the completion + of the GitHub Action. (#3759) ### Documentation diff --git a/action/main.py b/action/main.py index 1911cfd7a01..c0af3930dbb 100644 --- a/action/main.py +++ b/action/main.py @@ -1,5 +1,6 @@ import os import shlex +import shutil import sys from pathlib import Path from subprocess import PIPE, STDOUT, run @@ -73,5 +74,6 @@ stderr=STDOUT, encoding="utf-8", ) +shutil.rmtree(ENV_PATH, ignore_errors=True) print(proc.stdout) sys.exit(proc.returncode) From 66648c528a95553c1f822ece394ac98784baee47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:30:56 -0700 Subject: [PATCH 522/700] Bump pypa/cibuildwheel from 2.14.1 to 2.15.0 (#3836) --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 291193efc7a..9be231dd305 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.14.1 + uses: pypa/cibuildwheel@v2.15.0 env: CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" From 7c4fe83bd87ccef21f8c5a0cd5d122c5b004bb15 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 15 Aug 2023 06:51:26 -0700 Subject: [PATCH 523/700] Make pre-commit do less (#3838) --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60a092f8b29..a7ae7761494 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,3 +63,6 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace + +ci: + autoupdate_schedule: quarterly From ade371fd1c7118b8a82b281c28425fefb8cb719e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 00:01:21 -0700 Subject: [PATCH 524/700] [pre-commit.ci] pre-commit autoupdate (#3837) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7ae7761494..6301526a445 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.0 hooks: - id: mypy exclude: ^docs/conf.py From 793c2b5f9f7c7ca267fbcab58d30997ac6b9497d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:32:47 -0700 Subject: [PATCH 525/700] Pin tox to fix CI (#3843) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bf687435b4..8a139387c5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: - name: Install tox run: | python -m pip install --upgrade pip - python -m pip install --upgrade tox + python -m pip install --upgrade 'tox<4.7' - name: Unit tests if: "!startsWith(matrix.python-version, 'pypy')" From c6a031e623c7991ac9129f578dc21dffe2d7ede3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 19 Aug 2023 04:26:36 +0200 Subject: [PATCH 526/700] Improve caching by comparing file hashes as fallback for mtime and size (#3821) Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- CHANGES.md | 1 + .../reference/reference_classes.rst | 7 + .../reference/reference_functions.rst | 8 - pyproject.toml | 2 +- src/black/__init__.py | 11 +- src/black/cache.py | 160 +++++++++++------- src/black/concurrency.py | 9 +- tests/test_black.py | 155 +++++++++++------ 8 files changed, 219 insertions(+), 134 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8bf4188606c..a14a55a03ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ - Avoid importing `IPython` if notebook cells do not contain magics (#3782) +- Improve caching by comparing file hashes as fallback for mtime and size. (#3821) ### Output diff --git a/docs/contributing/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst index 29b25003af2..dc615579e30 100644 --- a/docs/contributing/reference/reference_classes.rst +++ b/docs/contributing/reference/reference_classes.rst @@ -186,6 +186,13 @@ Black Classes :show-inheritance: :members: +:class:`Cache` +------------------------ + +.. autoclass:: black.cache.Cache + :show-inheritance: + :members: + Enum Classes ~~~~~~~~~~~~~ diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index 09517f73961..dd92e37a7d4 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -94,18 +94,10 @@ Split functions Caching ------- -.. autofunction:: black.cache.filter_cached - .. autofunction:: black.cache.get_cache_dir .. autofunction:: black.cache.get_cache_file -.. autofunction:: black.cache.get_cache_info - -.. autofunction:: black.cache.read_cache - -.. autofunction:: black.cache.write_cache - Utilities --------- diff --git a/pyproject.toml b/pyproject.toml index d29b768c289..6cd3f34bc10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ "pathspec>=0.9.0", "platformdirs>=2", "tomli>=1.1.0; python_version < '3.11'", - "typing_extensions>=3.10.0.0; python_version < '3.10'", + "typing_extensions>=4.0.1; python_version < '3.11'", ] dynamic = ["readme", "version"] diff --git a/src/black/__init__.py b/src/black/__init__.py index 923a51867b5..dc06eab8dd0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -34,7 +34,7 @@ from pathspec.patterns.gitwildmatch import GitWildMatchPatternError from _black_version import version as __version__ -from black.cache import Cache, get_cache_info, read_cache, write_cache +from black.cache import Cache from black.comments import normalize_fmt_off from black.const import ( DEFAULT_EXCLUDES, @@ -775,12 +775,9 @@ def reformat_one( if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode): changed = Changed.YES else: - cache: Cache = {} + cache = Cache.read(mode) if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - cache = read_cache(mode) - res_src = src.resolve() - res_src_s = str(res_src) - if res_src_s in cache and cache[res_src_s] == get_cache_info(res_src): + if not cache.is_changed(src): changed = Changed.CACHED if changed is not Changed.CACHED and format_file_in_place( src, fast=fast, write_back=write_back, mode=mode @@ -789,7 +786,7 @@ def reformat_one( if (write_back is WriteBack.YES and changed is not Changed.CACHED) or ( write_back is WriteBack.CHECK and changed is Changed.NO ): - write_cache(cache, [src], mode) + cache.write([src]) report.done(src, changed) except Exception as exc: if report.verbose: diff --git a/src/black/cache.py b/src/black/cache.py index 9455ff44772..ff15da2a94e 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -1,21 +1,28 @@ """Caching of formatted files with feature-based invalidation.""" - +import hashlib import os import pickle +import sys import tempfile +from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, Iterable, Set, Tuple +from typing import Dict, Iterable, NamedTuple, Set, Tuple from platformdirs import user_cache_dir from _black_version import version as __version__ from black.mode import Mode -# types -Timestamp = float -FileSize = int -CacheInfo = Tuple[Timestamp, FileSize] -Cache = Dict[str, CacheInfo] +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +class FileData(NamedTuple): + st_mtime: float + st_size: int + hash: str def get_cache_dir() -> Path: @@ -37,61 +44,92 @@ def get_cache_dir() -> Path: CACHE_DIR = get_cache_dir() -def read_cache(mode: Mode) -> Cache: - """Read the cache if it exists and is well formed. - - If it is not well formed, the call to write_cache later should resolve the issue. - """ - cache_file = get_cache_file(mode) - if not cache_file.exists(): - return {} - - with cache_file.open("rb") as fobj: - try: - cache: Cache = pickle.load(fobj) - except (pickle.UnpicklingError, ValueError, IndexError): - return {} - - return cache - - def get_cache_file(mode: Mode) -> Path: return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle" -def get_cache_info(path: Path) -> CacheInfo: - """Return the information used to check if a file is already formatted or not.""" - stat = path.stat() - return stat.st_mtime, stat.st_size - - -def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]: - """Split an iterable of paths in `sources` into two sets. - - The first contains paths of files that modified on disk or are not in the - cache. The other contains paths to non-modified files. - """ - todo, done = set(), set() - for src in sources: - res_src = src.resolve() - if cache.get(str(res_src)) != get_cache_info(res_src): - todo.add(src) - else: - done.add(src) - return todo, done - - -def write_cache(cache: Cache, sources: Iterable[Path], mode: Mode) -> None: - """Update the cache file.""" - cache_file = get_cache_file(mode) - try: - CACHE_DIR.mkdir(parents=True, exist_ok=True) - new_cache = { - **cache, - **{str(src.resolve()): get_cache_info(src) for src in sources}, - } - with tempfile.NamedTemporaryFile(dir=str(cache_file.parent), delete=False) as f: - pickle.dump(new_cache, f, protocol=4) - os.replace(f.name, cache_file) - except OSError: - pass +@dataclass +class Cache: + mode: Mode + cache_file: Path + file_data: Dict[str, FileData] = field(default_factory=dict) + + @classmethod + def read(cls, mode: Mode) -> Self: + """Read the cache if it exists and is well formed. + + If it is not well formed, the call to write later should + resolve the issue. + """ + cache_file = get_cache_file(mode) + if not cache_file.exists(): + return cls(mode, cache_file) + + with cache_file.open("rb") as fobj: + try: + file_data: Dict[str, FileData] = pickle.load(fobj) + except (pickle.UnpicklingError, ValueError, IndexError): + return cls(mode, cache_file) + + return cls(mode, cache_file, file_data) + + @staticmethod + def hash_digest(path: Path) -> str: + """Return hash digest for path.""" + + data = path.read_bytes() + return hashlib.sha256(data).hexdigest() + + @staticmethod + def get_file_data(path: Path) -> FileData: + """Return file data for path.""" + + stat = path.stat() + hash = Cache.hash_digest(path) + return FileData(stat.st_mtime, stat.st_size, hash) + + def is_changed(self, source: Path) -> bool: + """Check if source has changed compared to cached version.""" + res_src = source.resolve() + old = self.file_data.get(str(res_src)) + if old is None: + return True + + st = res_src.stat() + if st.st_size != old.st_size: + return True + if int(st.st_mtime) != int(old.st_mtime): + new_hash = Cache.hash_digest(res_src) + if new_hash != old.hash: + return True + return False + + def filtered_cached(self, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]: + """Split an iterable of paths in `sources` into two sets. + + The first contains paths of files that modified on disk or are not in the + cache. The other contains paths to non-modified files. + """ + changed: Set[Path] = set() + done: Set[Path] = set() + for src in sources: + if self.is_changed(src): + changed.add(src) + else: + done.add(src) + return changed, done + + def write(self, sources: Iterable[Path]) -> None: + """Update the cache file data and write a new cache file.""" + self.file_data.update( + **{str(src.resolve()): Cache.get_file_data(src) for src in sources} + ) + try: + CACHE_DIR.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + dir=str(self.cache_file.parent), delete=False + ) as f: + pickle.dump(self.file_data, f, protocol=4) + os.replace(f.name, self.cache_file) + except OSError: + pass diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 893eba6675a..ce016578399 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -17,7 +17,7 @@ from mypy_extensions import mypyc_attr from black import WriteBack, format_file_in_place -from black.cache import Cache, filter_cached, read_cache, write_cache +from black.cache import Cache from black.mode import Mode from black.output import err from black.report import Changed, Report @@ -133,10 +133,9 @@ async def schedule_formatting( `write_back`, `fast`, and `mode` options are passed to :func:`format_file_in_place`. """ - cache: Cache = {} + cache = Cache.read(mode) if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): - cache = read_cache(mode) - sources, cached = filter_cached(cache, sources) + sources, cached = cache.filtered_cached(sources) for src in sorted(cached): report.done(src, Changed.CACHED) if not sources: @@ -185,4 +184,4 @@ async def schedule_formatting( if cancelled: await asyncio.gather(*cancelled, return_exceptions=True) if sources_to_cache: - write_cache(cache, sources_to_cache, mode) + cache.write(sources_to_cache) diff --git a/tests/test_black.py b/tests/test_black.py index 3b3ab721c5f..8ae92172d43 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -41,7 +41,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_dir, get_cache_file +from black.cache import FileData, get_cache_dir, get_cache_file from black.debug import DebugVisitor from black.output import color_diff, diff from black.report import Report @@ -1121,10 +1121,10 @@ def test_single_file_force_pyi(self) -> None: self.invokeBlack([str(path), "--pyi"]) actual = path.read_text(encoding="utf-8") # verify cache with --pyi is separate - pyi_cache = black.read_cache(pyi_mode) - self.assertIn(str(path), pyi_cache) - normal_cache = black.read_cache(DEFAULT_MODE) - self.assertNotIn(str(path), normal_cache) + pyi_cache = black.Cache.read(pyi_mode) + assert not pyi_cache.is_changed(path) + normal_cache = black.Cache.read(DEFAULT_MODE) + assert normal_cache.is_changed(path) self.assertFormatEqual(expected, actual) black.assert_equivalent(contents, actual) black.assert_stable(contents, actual, pyi_mode) @@ -1146,11 +1146,11 @@ def test_multi_file_force_pyi(self) -> None: actual = path.read_text(encoding="utf-8") self.assertEqual(actual, expected) # verify cache with --pyi is separate - pyi_cache = black.read_cache(pyi_mode) - normal_cache = black.read_cache(reg_mode) + pyi_cache = black.Cache.read(pyi_mode) + normal_cache = black.Cache.read(reg_mode) for path in paths: - self.assertIn(str(path), pyi_cache) - self.assertNotIn(str(path), normal_cache) + assert not pyi_cache.is_changed(path) + assert normal_cache.is_changed(path) def test_pipe_force_pyi(self) -> None: source, expected = read_data("miscellaneous", "force_pyi") @@ -1171,10 +1171,10 @@ def test_single_file_force_py36(self) -> None: self.invokeBlack([str(path), *PY36_ARGS]) actual = path.read_text(encoding="utf-8") # verify cache with --target-version is separate - py36_cache = black.read_cache(py36_mode) - self.assertIn(str(path), py36_cache) - normal_cache = black.read_cache(reg_mode) - self.assertNotIn(str(path), normal_cache) + py36_cache = black.Cache.read(py36_mode) + assert not py36_cache.is_changed(path) + normal_cache = black.Cache.read(reg_mode) + assert normal_cache.is_changed(path) self.assertEqual(actual, expected) @event_loop() @@ -1194,11 +1194,11 @@ def test_multi_file_force_py36(self) -> None: actual = path.read_text(encoding="utf-8") self.assertEqual(actual, expected) # verify cache with --target-version is separate - pyi_cache = black.read_cache(py36_mode) - normal_cache = black.read_cache(reg_mode) + pyi_cache = black.Cache.read(py36_mode) + normal_cache = black.Cache.read(reg_mode) for path in paths: - self.assertIn(str(path), pyi_cache) - self.assertNotIn(str(path), normal_cache) + assert not pyi_cache.is_changed(path) + assert normal_cache.is_changed(path) def test_pipe_force_py36(self) -> None: source, expected = read_data("miscellaneous", "force_py36") @@ -1953,19 +1953,20 @@ def test_cache_broken_file(self) -> None: with cache_dir() as workspace: cache_file = get_cache_file(mode) cache_file.write_text("this is not a pickle", encoding="utf-8") - assert black.read_cache(mode) == {} + assert black.Cache.read(mode).file_data == {} src = (workspace / "test.py").resolve() src.write_text("print('hello')", encoding="utf-8") invokeBlack([str(src)]) - cache = black.read_cache(mode) - assert str(src) in cache + cache = black.Cache.read(mode) + assert not cache.is_changed(src) def test_cache_single_file_already_cached(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.write_text("print('hello')", encoding="utf-8") - black.write_cache({}, [src], mode) + cache = black.Cache.read(mode) + cache.write([src]) invokeBlack([str(src)]) assert src.read_text(encoding="utf-8") == "print('hello')" @@ -1979,13 +1980,14 @@ def test_cache_multiple_files(self) -> None: one.write_text("print('hello')", encoding="utf-8") two = (workspace / "two.py").resolve() two.write_text("print('hello')", encoding="utf-8") - black.write_cache({}, [one], mode) + cache = black.Cache.read(mode) + cache.write([one]) invokeBlack([str(workspace)]) assert one.read_text(encoding="utf-8") == "print('hello')" assert two.read_text(encoding="utf-8") == 'print("hello")\n' - cache = black.read_cache(mode) - assert str(one) in cache - assert str(two) in cache + cache = black.Cache.read(mode) + assert not cache.is_changed(one) + assert not cache.is_changed(two) @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"]) def test_no_cache_when_writeback_diff(self, color: bool) -> None: @@ -1993,8 +1995,8 @@ def test_no_cache_when_writeback_diff(self, color: bool) -> None: with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.write_text("print('hello')", encoding="utf-8") - with patch("black.read_cache") as read_cache, patch( - "black.write_cache" + with patch.object(black.Cache, "read") as read_cache, patch.object( + black.Cache, "write" ) as write_cache: cmd = [str(src), "--diff"] if color: @@ -2002,8 +2004,8 @@ def test_no_cache_when_writeback_diff(self, color: bool) -> None: invokeBlack(cmd) cache_file = get_cache_file(mode) assert cache_file.exists() is False + read_cache.assert_called_once() write_cache.assert_not_called() - read_cache.assert_not_called() @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"]) @event_loop() @@ -2036,17 +2038,17 @@ def test_no_cache_when_stdin(self) -> None: def test_read_cache_no_cachefile(self) -> None: mode = DEFAULT_MODE with cache_dir(): - assert black.read_cache(mode) == {} + assert black.Cache.read(mode).file_data == {} def test_write_cache_read_cache(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.touch() - black.write_cache({}, [src], mode) - cache = black.read_cache(mode) - assert str(src) in cache - assert cache[str(src)] == black.get_cache_info(src) + write_cache = black.Cache.read(mode) + write_cache.write([src]) + read_cache = black.Cache.read(mode) + assert not read_cache.is_changed(src) def test_filter_cached(self) -> None: with TemporaryDirectory() as workspace: @@ -2057,21 +2059,67 @@ def test_filter_cached(self) -> None: uncached.touch() cached.touch() cached_but_changed.touch() - cache = { - str(cached): black.get_cache_info(cached), - str(cached_but_changed): (0.0, 0), - } - todo, done = black.cache.filter_cached( - cache, {uncached, cached, cached_but_changed} - ) + cache = black.Cache.read(DEFAULT_MODE) + + orig_func = black.Cache.get_file_data + + def wrapped_func(path: Path) -> FileData: + if path == cached: + return orig_func(path) + if path == cached_but_changed: + return FileData(0.0, 0, "") + raise AssertionError + + with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func): + cache.write([cached, cached_but_changed]) + todo, done = cache.filtered_cached({uncached, cached, cached_but_changed}) assert todo == {uncached, cached_but_changed} assert done == {cached} + def test_filter_cached_hash(self) -> None: + with TemporaryDirectory() as workspace: + path = Path(workspace) + src = (path / "test.py").resolve() + src.write_text("print('hello')", encoding="utf-8") + st = src.stat() + cache = black.Cache.read(DEFAULT_MODE) + cache.write([src]) + cached_file_data = cache.file_data[str(src)] + + todo, done = cache.filtered_cached([src]) + assert todo == set() + assert done == {src} + assert cached_file_data.st_mtime == st.st_mtime + + # Modify st_mtime + cached_file_data = cache.file_data[str(src)] = FileData( + cached_file_data.st_mtime - 1, + cached_file_data.st_size, + cached_file_data.hash, + ) + todo, done = cache.filtered_cached([src]) + assert todo == set() + assert done == {src} + assert cached_file_data.st_mtime < st.st_mtime + assert cached_file_data.st_size == st.st_size + assert cached_file_data.hash == black.Cache.hash_digest(src) + + # Modify contents + src.write_text("print('hello world')", encoding="utf-8") + new_st = src.stat() + todo, done = cache.filtered_cached([src]) + assert todo == {src} + assert done == set() + assert cached_file_data.st_mtime < new_st.st_mtime + assert cached_file_data.st_size != new_st.st_size + assert cached_file_data.hash != black.Cache.hash_digest(src) + def test_write_cache_creates_directory_if_needed(self) -> None: mode = DEFAULT_MODE with cache_dir(exists=False) as workspace: assert not workspace.exists() - black.write_cache({}, [], mode) + cache = black.Cache.read(mode) + cache.write([]) assert workspace.exists() @event_loop() @@ -2085,15 +2133,17 @@ def test_failed_formatting_does_not_get_cached(self) -> None: clean = (workspace / "clean.py").resolve() clean.write_text('print("hello")\n', encoding="utf-8") invokeBlack([str(workspace)], exit_code=123) - cache = black.read_cache(mode) - assert str(failing) not in cache - assert str(clean) in cache + cache = black.Cache.read(mode) + assert cache.is_changed(failing) + assert not cache.is_changed(clean) def test_write_cache_write_fail(self) -> None: mode = DEFAULT_MODE - with cache_dir(), patch.object(Path, "open") as mock: - mock.side_effect = OSError - black.write_cache({}, [], mode) + with cache_dir(): + cache = black.Cache.read(mode) + with patch.object(Path, "open") as mock: + mock.side_effect = OSError + cache.write([]) def test_read_cache_line_lengths(self) -> None: mode = DEFAULT_MODE @@ -2101,11 +2151,12 @@ def test_read_cache_line_lengths(self) -> None: with cache_dir() as workspace: path = (workspace / "file.py").resolve() path.touch() - black.write_cache({}, [path], mode) - one = black.read_cache(mode) - assert str(path) in one - two = black.read_cache(short_mode) - assert str(path) not in two + cache = black.Cache.read(mode) + cache.write([path]) + one = black.Cache.read(mode) + assert not one.is_changed(path) + two = black.Cache.read(short_mode) + assert two.is_changed(path) def assert_collected_sources( From 066aa9210ac7815cbb9b4a25075f54d614b0afc7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:09:59 +0200 Subject: [PATCH 527/700] Remove tox pin (#3844) --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a139387c5b..216b0ba5236 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,11 +45,12 @@ jobs: - name: Install tox run: | python -m pip install --upgrade pip - python -m pip install --upgrade 'tox<4.7' + python -m pip install --upgrade tox - name: Unit tests if: "!startsWith(matrix.python-version, 'pypy')" - run: tox -e ci-py -- -v --color=yes + run: + tox -e ci-py$(echo ${{ matrix.python-version }} | tr -d '.') -- -v --color=yes - name: Unit tests (pypy) if: "startsWith(matrix.python-version, 'pypy')" From 6310a405f6663948f7e0b9411cb54e5db2b712a6 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 19 Aug 2023 08:13:05 -0700 Subject: [PATCH 528/700] Improve handling of root to get_sources (#3847) This is a little more type safe and a little cleaner --- src/black/__init__.py | 14 ++++++++------ tests/test_black.py | 28 ++++++++-------------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index dc06eab8dd0..6fc91d2e6d3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -560,9 +560,10 @@ def main( # noqa: C901 content=code, fast=fast, write_back=write_back, mode=mode, report=report ) else: + assert root is not None # root is only None if code is not None try: sources = get_sources( - ctx=ctx, + root=root, src=src, quiet=quiet, verbose=verbose, @@ -615,7 +616,7 @@ def main( # noqa: C901 def get_sources( *, - ctx: click.Context, + root: Path, src: Tuple[str, ...], quiet: bool, verbose: bool, @@ -628,7 +629,6 @@ def get_sources( ) -> Set[Path]: """Compute the set of files to be formatted.""" sources: Set[Path] = set() - root = ctx.obj["root"] using_default_exclude = exclude is None exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude @@ -645,7 +645,7 @@ def get_sources( if is_stdin or p.is_file(): normalized_path: Optional[str] = normalize_path_maybe_ignore( - p, ctx.obj["root"], report + p, root, report ) if normalized_path is None: if verbose: @@ -674,7 +674,9 @@ def get_sources( sources.add(p) elif p.is_dir(): - p = root / normalize_path_maybe_ignore(p, ctx.obj["root"], report) + p_relative = normalize_path_maybe_ignore(p, root, report) + assert p_relative is not None + p = root / p_relative if verbose: out(f'Found input source directory: "{p}"', fg="blue") @@ -686,7 +688,7 @@ def get_sources( sources.update( gen_python_files( p.iterdir(), - ctx.obj["root"], + root, include, exclude, extend_exclude, diff --git a/tests/test_black.py b/tests/test_black.py index 8ae92172d43..79930fabf1f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -507,13 +507,11 @@ def _mocked_calls() -> bool: with patch("pathlib.Path.iterdir", return_value=target_contents), patch( "pathlib.Path.cwd", return_value=working_directory ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])): - ctx = FakeContext() # Note that the root folder (project_root) isn't the folder # named "root" (aka working_directory) - ctx.obj["root"] = project_root report = MagicMock(verbose=True) black.get_sources( - ctx=ctx, + root=project_root, src=("./child",), quiet=False, verbose=True, @@ -2163,7 +2161,7 @@ def assert_collected_sources( src: Sequence[Union[str, Path]], expected: Sequence[Union[str, Path]], *, - ctx: Optional[FakeContext] = None, + root: Optional[Path] = None, exclude: Optional[str] = None, include: Optional[str] = None, extend_exclude: Optional[str] = None, @@ -2179,7 +2177,7 @@ def assert_collected_sources( ) gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude) collected = black.get_sources( - ctx=ctx or FakeContext(), + root=root or THIS_DIR, src=gs_src, quiet=False, verbose=False, @@ -2215,9 +2213,7 @@ def test_gitignore_used_as_default(self) -> None: base / "b/.definitely_exclude/a.pyi", ] src = [base / "b/"] - ctx = FakeContext() - ctx.obj["root"] = base - assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/") + assert_collected_sources(src, expected, root=base, extend_exclude=r"/exclude/") def test_gitignore_used_on_multiple_sources(self) -> None: root = Path(DATA_DIR / "gitignore_used_on_multiple_sources") @@ -2225,10 +2221,8 @@ def test_gitignore_used_on_multiple_sources(self) -> None: root / "dir1" / "b.py", root / "dir2" / "b.py", ] - ctx = FakeContext() - ctx.obj["root"] = root src = [root / "dir1", root / "dir2"] - assert_collected_sources(src, expected, ctx=ctx) + assert_collected_sources(src, expected, root=root) @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_exclude_for_issue_1572(self) -> None: @@ -2334,9 +2328,7 @@ def test_gitignore_that_ignores_subfolders(self) -> None: # If gitignore with */* is in root root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir") expected = [root / "b.py"] - ctx = FakeContext() - ctx.obj["root"] = root - assert_collected_sources([root], expected, ctx=ctx) + assert_collected_sources([root], expected, root=root) # If .gitignore with */* is nested root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") @@ -2344,17 +2336,13 @@ def test_gitignore_that_ignores_subfolders(self) -> None: root / "a.py", root / "subdir" / "b.py", ] - ctx = FakeContext() - ctx.obj["root"] = root - assert_collected_sources([root], expected, ctx=ctx) + assert_collected_sources([root], expected, root=root) # If command is executed from outer dir root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") target = root / "subdir" expected = [target / "b.py"] - ctx = FakeContext() - ctx.obj["root"] = root - assert_collected_sources([target], expected, ctx=ctx) + assert_collected_sources([target], expected, root=root) def test_empty_include(self) -> None: path = DATA_DIR / "include_exclude_tests" From d9c249c25a77f75e70278aab9ec65c10ce08b0a8 Mon Sep 17 00:00:00 2001 From: Kjell-Magnus Date: Tue, 22 Aug 2023 21:40:10 +0200 Subject: [PATCH 529/700] Fix download badge link (#3853) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d0b29af215..b257c333f0d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Coverage Status License: MIT PyPI -Downloads +Downloads conda-forge Code style: black

From 47676bf5939ae5c8e670d947917bc8af4732eab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:44:17 -0500 Subject: [PATCH 530/700] Bump furo from 2023.7.26 to 2023.8.19 in /docs + sphinx to 7.2.3 (#3848) * Bump furo from 2023.7.26 to 2023.8.19 in /docs Bumps [furo](https://github.com/pradyunsg/furo) from 2023.7.26 to 2023.8.19. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.07.26...2023.08.19) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Move to sphinx 7.2.3 + fix intersphinx_mapping --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cooper Ry Lees --- docs/conf.py | 2 +- docs/requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 7fc4f8f589e..f7cf1b42842 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -213,4 +213,4 @@ def make_pypi_svg(version: str) -> None: # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/3/": None} +intersphinx_mapping = {"": ("https://docs.python.org/3/", None)} diff --git a/docs/requirements.txt b/docs/requirements.txt index ff179f3805e..dad39f67ed3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,9 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==2.0.0 -Sphinx==6.1.3 +Sphinx==7.2.3 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.2 -furo==2023.7.26 +furo==2023.8.19 From 58f1bf69d2ed2f6e3e5fa6a31e01ae58c9ffcff9 Mon Sep 17 00:00:00 2001 From: "Johnny.H" Date: Sun, 3 Sep 2023 10:46:23 +0800 Subject: [PATCH 531/700] Move coverage configurations to `pyproject.toml` (#3858) --- .coveragerc | 9 --------- .github/workflows/test.yml | 4 ++-- pyproject.toml | 9 +++++++++ 3 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5577e496a57..00000000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[report] -omit = - src/blib2to3/* - tests/data/* - */site-packages/* - .tox/* - -[run] -relative_files = True diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 216b0ba5236..7daa31ee903 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: if: github.repository == 'psf/black' && matrix.os == 'ubuntu-latest' && !startsWith(matrix.python-version, 'pypy') - uses: AndreMiras/coveralls-python-action@v20201129 + uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Send finished signal to Coveralls - uses: AndreMiras/coveralls-python-action@v20201129 + uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99 with: parallel-finished: true debug: true diff --git a/pyproject.toml b/pyproject.toml index 6cd3f34bc10..ea5c9f84684 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -210,3 +210,12 @@ filterwarnings = [ # Work around https://github.com/pytest-dev/pytest/issues/10977 for Python 3.12 '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''' ] +[tool.coverage.report] +omit = [ + "src/blib2to3/*", + "tests/data/*", + "*/site-packages/*", + ".tox/*" +] +[tool.coverage.run] +relative_files = true From df50fee7fd85018f8db462774512a83031f00322 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 6 Sep 2023 21:06:07 -0700 Subject: [PATCH 532/700] Apply ignore logic before symlink resolution (#3846) This means, for instance, that a gitignored symlink cannot affect your formatting. Fixes #3527, fixes #3826 --- CHANGES.md | 2 ++ src/black/files.py | 20 ++++++++------- tests/test_black.py | 62 +++++++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a14a55a03ac..2168c1b90ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ +- Black now applies exclusion and ignore logic before resolving symlinks (#3846) + ### Packaging diff --git a/src/black/files.py b/src/black/files.py index 368e4170d47..362898dc0fd 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -330,35 +330,37 @@ def gen_python_files( assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: - normalized_path = normalize_path_maybe_ignore(child, root, report) - if normalized_path is None: - continue + root_relative_path = child.absolute().relative_to(root).as_posix() # First ignore files matching .gitignore, if passed if gitignore_dict and _path_is_ignored( - normalized_path, root, gitignore_dict, report + root_relative_path, root, gitignore_dict, report ): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. - normalized_path = "/" + normalized_path + root_relative_path = "/" + root_relative_path if child.is_dir(): - normalized_path += "/" + root_relative_path += "/" - if path_is_excluded(normalized_path, exclude): + if path_is_excluded(root_relative_path, exclude): report.path_ignored(child, "matches the --exclude regular expression") continue - if path_is_excluded(normalized_path, extend_exclude): + if path_is_excluded(root_relative_path, extend_exclude): report.path_ignored( child, "matches the --extend-exclude regular expression" ) continue - if path_is_excluded(normalized_path, force_exclude): + if path_is_excluded(root_relative_path, force_exclude): report.path_ignored(child, "matches the --force-exclude regular expression") continue + normalized_path = normalize_path_maybe_ignore(child, root, report) + if normalized_path is None: + continue + if child.is_dir(): # If gitignore is None, gitignore usage is disabled, while a Falsey # gitignore is when the directory doesn't have a .gitignore file. diff --git a/tests/test_black.py b/tests/test_black.py index 79930fabf1f..4fb6aef9bca 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -492,9 +492,7 @@ def test_false_positive_symlink_output_issue_3384(self) -> None: project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests") working_directory = project_root / "root" target_abspath = working_directory / "child" - target_contents = ( - src.relative_to(working_directory) for src in target_abspath.iterdir() - ) + target_contents = list(target_abspath.iterdir()) def mock_n_calls(responses: List[bool]) -> Callable[[], bool]: def _mocked_calls() -> bool: @@ -2375,38 +2373,48 @@ def test_extend_exclude(self) -> None: ) @pytest.mark.incompatible_with_mypyc - def test_symlink_out_of_root_directory(self) -> None: + def test_symlinks(self) -> None: path = MagicMock() root = THIS_DIR.resolve() - child = MagicMock() include = re.compile(black.DEFAULT_INCLUDES) exclude = re.compile(black.DEFAULT_EXCLUDES) report = black.Report() gitignore = PathSpec.from_lines("gitwildmatch", []) - # `child` should behave like a symlink which resolved path is clearly - # outside of the `root` directory. - path.iterdir.return_value = [child] - child.resolve.return_value = Path("/a/b/c") - child.as_posix.return_value = "/a/b/c" - try: - list( - black.gen_python_files( - path.iterdir(), - root, - include, - exclude, - None, - None, - report, - {path: gitignore}, - verbose=False, - quiet=False, - ) + + regular = MagicMock() + outside_root_symlink = MagicMock() + ignored_symlink = MagicMock() + + path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink] + + regular.absolute.return_value = root / "regular.py" + regular.resolve.return_value = root / "regular.py" + regular.is_dir.return_value = False + + outside_root_symlink.absolute.return_value = root / "symlink.py" + outside_root_symlink.resolve.return_value = Path("/nowhere") + + ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py" + + files = list( + black.gen_python_files( + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + {path: gitignore}, + verbose=False, + quiet=False, ) - except ValueError as ve: - pytest.fail(f"`get_python_files_in_dir()` failed: {ve}") + ) + assert files == [regular] + path.iterdir.assert_called_once() - child.resolve.assert_called_once() + outside_root_symlink.resolve.assert_called_once() + ignored_symlink.resolve.assert_not_called() @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin(self) -> None: From 8daa64a2e10907539094df51f4c51306bb426f07 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:11:50 +1000 Subject: [PATCH 533/700] blackd: fix mishandling of single character input (#3558) --- CHANGES.md | 2 ++ src/blackd/__init__.py | 3 ++- tests/test_blackd.py | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2168c1b90ce..af9fc490acf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,8 @@ +- Fix an issue in `blackd` with single character input (#3558) + ### Integrations diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 4f2d87d0fca..6b0f3d33295 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -152,7 +152,8 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: ) # Preserve CRLF line endings - if req_str[req_str.find("\n") - 1] == "\r": + nl = req_str.find("\n") + if nl > 0 and req_str[nl - 1] == "\r": formatted_str = formatted_str.replace("\n", "\r\n") # If, after swapping line endings, nothing changed, then say so if formatted_str == req_str: diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 325bd7dd5aa..dd2126e6bc2 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -240,3 +240,9 @@ async def test_normalizes_line_endings(self) -> None: response = await self.client.post("/", data=data) self.assertEqual(await response.text(), expected) self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_single_character(self) -> None: + response = await self.client.post("/", data="1") + self.assertEqual(await response.text(), "1\n") + self.assertEqual(response.status, 200) From 74d3009ba480a871df57197144578f1ae4016210 Mon Sep 17 00:00:00 2001 From: Jonas Haag Date: Fri, 8 Sep 2023 03:35:07 +0200 Subject: [PATCH 534/700] Add Black PyCharm 2023.2 integration instructions (#3839) --- docs/integrations/editors.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index ff563068e79..cebe2b0721e 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -10,16 +10,26 @@ Options include the following: ## PyCharm/IntelliJ IDEA -There are three different ways you can use _Black_ from PyCharm: +There are several different ways you can use _Black_ from PyCharm: -1. As local server using the BlackConnect plugin -1. As external tool -1. As file watcher +1. Using the built-in _Black_ integration (PyCharm 2023.2 and later). This option is the + simplest to set up. +1. As local server using the BlackConnect plugin. This option formats the fastest. It + spins up {doc}`Black's HTTP server `, to + avoid the startup cost on subsequent formats. +1. As external tool. +1. As file watcher. -The first option is the simplest to set up and formats the fastest (by spinning up -{doc}`Black's HTTP server `, avoiding the -startup cost on subsequent formats), but if you would prefer to not install a -third-party plugin or blackd's extra dependencies, the other two are also great options. +### Built-in _Black_ integration + +1. Install `black`. + + ```console + $ pip install black + ``` + +1. Go to `Preferences or Settings -> Tools -> Black` and configure _Black_ to your + liking. ### As local server From a20338cf100ff20a24e2058c6f6014e82efdf914 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 8 Sep 2023 16:37:13 +0200 Subject: [PATCH 535/700] Avoid removing whitespace for walrus operators within subscripts (#3823) Co-authored-by: hauntsaninja Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/lines.py | 4 +++- src/black/mode.py | 1 + src/black/nodes.py | 8 +++++++- tests/data/preview/pep_572.py | 6 ++++++ tests/data/preview_py_310/pep_572.py | 12 ++++++++++++ tests/test_format.py | 7 +++++++ 7 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/data/preview/pep_572.py create mode 100644 tests/data/preview_py_310/pep_572.py diff --git a/CHANGES.md b/CHANGES.md index af9fc490acf..4aa3123fab6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -80,6 +80,7 @@ (#3740) - Fix error in AST validation when _Black_ removes trailing whitespace in a type comment (#3773) +- Fix a bug whereby spaces were removed from walrus operators within subscript (#3823) ### Preview style diff --git a/src/black/lines.py b/src/black/lines.py index 0a307b45eff..f3044ce47b8 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -81,7 +81,9 @@ def append( # Note: at this point leaf.prefix should be empty except for # imports, for which we only preserve newlines. leaf.prefix += whitespace( - leaf, complex_subscript=self.is_complex_subscript(leaf) + leaf, + complex_subscript=self.is_complex_subscript(leaf), + mode=self.mode, ) if self.inside_brackets or not preformatted or track_bracket: self.bracket_tracker.mark(leaf) diff --git a/src/black/mode.py b/src/black/mode.py index 282c1669da7..06d20b7a7d6 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -183,6 +183,7 @@ class Preview(Enum): wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() dummy_implementations = auto() + walrus_subscript = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index 45423b2596b..edd201a21e9 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -13,6 +13,7 @@ from mypy_extensions import mypyc_attr from black.cache import CACHE_DIR +from black.mode import Mode, Preview from black.strings import has_triple_quotes from blib2to3 import pygram from blib2to3.pgen2 import token @@ -171,7 +172,7 @@ def visit_default(self, node: LN) -> Iterator[T]: yield from self.visit(child) -def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 +def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # noqa: C901 """Return whitespace prefix if needed for the given `leaf`. `complex_subscript` signals whether the given leaf is part of a subscription @@ -345,6 +346,11 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 return NO + elif Preview.walrus_subscript in mode and ( + t == token.COLONEQUAL or prev.type == token.COLONEQUAL + ): + return SPACE + elif not complex_subscript: return NO diff --git a/tests/data/preview/pep_572.py b/tests/data/preview/pep_572.py new file mode 100644 index 00000000000..a50e130ad9c --- /dev/null +++ b/tests/data/preview/pep_572.py @@ -0,0 +1,6 @@ +x[(a:=0):] +x[:(a:=0)] + +# output +x[(a := 0):] +x[:(a := 0)] diff --git a/tests/data/preview_py_310/pep_572.py b/tests/data/preview_py_310/pep_572.py new file mode 100644 index 00000000000..78d4e9e4506 --- /dev/null +++ b/tests/data/preview_py_310/pep_572.py @@ -0,0 +1,12 @@ +x[a:=0] +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] +x[a:=0,b:=1] + +# output +x[a := 0] +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] +x[a := 0, b := 1] diff --git a/tests/test_format.py b/tests/test_format.py index fb4d8eb4346..0650a2d6e53 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -56,6 +56,13 @@ def test_preview_context_managers_targeting_py39() -> None: assert_format(source, expected, mode, minimum_version=(3, 9)) +@pytest.mark.parametrize("filename", all_data_cases("preview_py_310")) +def test_preview_python_310(filename: str) -> None: + source, expected = read_data("preview_py_310", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY310}, preview=True) + assert_format(source, expected, mode, minimum_version=(3, 10)) + + @pytest.mark.parametrize( "filename", all_data_cases("preview_context_managers/auto_detect") ) From b40b01ffe3dbf1fa989acd6050ef5e61c085b5da Mon Sep 17 00:00:00 2001 From: konsti Date: Sat, 9 Sep 2023 03:51:27 +0200 Subject: [PATCH 536/700] Blank line between nested and function def in stub files. (#3862) The idea behind this change is that we stop looking into previous body to determine if there should be a blank before a function or class definition. Input: ```python import sys if sys.version_info > (3, 7): class Nested1: assignment = 1 def function_definition(self): ... def f1(self) -> str: ... class Nested2: def function_definition(self): ... assignment = 1 def f2(self) -> str: ... if sys.version_info > (3, 7): def nested1(): assignment = 1 def function_definition(self): ... def f1(self) -> str: ... def nested2(): def function_definition(self): ... assignment = 1 def f2(self) -> str: ... ``` Stable style ```python import sys if sys.version_info > (3, 7): class Nested1: assignment = 1 def function_definition(self): ... def f1(self) -> str: ... class Nested2: def function_definition(self): ... assignment = 1 def f2(self) -> str: ... if sys.version_info > (3, 7): def nested1(): assignment = 1 def function_definition(self): ... def f1(self) -> str: ... def nested2(): def function_definition(self): ... assignment = 1 def f2(self) -> str: ... ``` In the stable formatting, we have a blank line sometimes, not depending on the previous statement on the same level, but on the last (potentially nested) statement in the previous body. #2783/#3564 fixes this for classes in preview style: ```python import sys if sys.version_info > (3, 7): class Nested1: assignment = 1 def function_definition(self): ... def f1(self) -> str: ... class Nested2: def function_definition(self): ... assignment = 1 def f2(self) -> str: ... if sys.version_info > (3, 7): def nested1(): assignment = 1 def function_definition(self): ... def f1(self) -> str: ... def nested2(): def function_definition(self): ... assignment = 1 def f2(self) -> str: ... ``` This PR additionally fixes this for function definitions: ```python if sys.version_info > (3, 7): if sys.platform == "win32": assignment = 1 def function_definition(self): ... def f1(self) -> str: ... if sys.platform != "win32": def function_definition(self): ... assignment = 1 def f2(self) -> str: ... if sys.version_info > (3, 8): if sys.platform == "win32": assignment = 1 def function_definition(self): ... class F1: ... if sys.platform != "win32": def function_definition(self): ... assignment = 1 class F2: ... ``` You can see the effect of this change on typeshed in https://github.com/konstin/typeshed/pull/1/files. As baseline, the preview mode changes without this PR are at https://github.com/konstin/typeshed/pull/2. Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + src/black/lines.py | 11 +++++ src/black/mode.py | 1 + .../data/miscellaneous/nested_class_stub.pyi | 16 ------- tests/data/miscellaneous/nested_stub.pyi | 43 +++++++++++++++++++ tests/test_format.py | 4 +- 6 files changed, 59 insertions(+), 18 deletions(-) delete mode 100644 tests/data/miscellaneous/nested_class_stub.pyi create mode 100644 tests/data/miscellaneous/nested_stub.pyi diff --git a/CHANGES.md b/CHANGES.md index 4aa3123fab6..b0fa5f8745e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -171,6 +171,8 @@ expected to become part of Black's stable style in January 2024. - For stubs, enforce one blank line after a nested class with a body other than just `...` (#3564) - Improve handling of multiline strings by changing line split behavior (#1879) +- In stub files, add a blank line between a statement with a body (e.g an + `if sys.version_info > (3, x):`) and a function definition on the same level. (#3862) ### Parser diff --git a/src/black/lines.py b/src/black/lines.py index f3044ce47b8..71b657a0654 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -711,6 +711,17 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 newlines = 0 else: newlines = 1 + # Remove case `self.previous_line.depth > current_line.depth` below when + # this becomes stable. + # + # Don't inspect the previous line if it's part of the body of the previous + # statement in the same level, we always want a blank line if there's + # something with a body preceding. + elif ( + Preview.blank_line_between_nested_and_def_stub_file in current_line.mode + and self.previous_line.depth > current_line.depth + ): + newlines = 1 elif ( current_line.is_def or current_line.is_decorator ) and not self.previous_line.is_def: diff --git a/src/black/mode.py b/src/black/mode.py index 06d20b7a7d6..8a855ac495a 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -170,6 +170,7 @@ class Preview(Enum): add_trailing_comma_consistently = auto() blank_line_after_nested_stub_class = auto() + blank_line_between_nested_and_def_stub_file = auto() hex_codes_in_unicode_sequences = auto() improved_async_statements_handling = auto() multiline_string_handling = auto() diff --git a/tests/data/miscellaneous/nested_class_stub.pyi b/tests/data/miscellaneous/nested_class_stub.pyi deleted file mode 100644 index daf281b517b..00000000000 --- a/tests/data/miscellaneous/nested_class_stub.pyi +++ /dev/null @@ -1,16 +0,0 @@ -class Outer: - class InnerStub: ... - outer_attr_after_inner_stub: int - class Inner: - inner_attr: int - outer_attr: int - -# output -class Outer: - class InnerStub: ... - outer_attr_after_inner_stub: int - - class Inner: - inner_attr: int - - outer_attr: int diff --git a/tests/data/miscellaneous/nested_stub.pyi b/tests/data/miscellaneous/nested_stub.pyi new file mode 100644 index 00000000000..15e69d854db --- /dev/null +++ b/tests/data/miscellaneous/nested_stub.pyi @@ -0,0 +1,43 @@ +import sys + +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + class Inner: + inner_attr: int + outer_attr: int + +if sys.version_info > (3, 7): + if sys.platform == "win32": + assignment = 1 + def function_definition(self): ... + def f1(self) -> str: ... + if sys.platform != "win32": + def function_definition(self): ... + assignment = 1 + def f2(self) -> str: ... + +# output + +import sys + +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + + class Inner: + inner_attr: int + + outer_attr: int + +if sys.version_info > (3, 7): + if sys.platform == "win32": + assignment = 1 + def function_definition(self): ... + + def f1(self) -> str: ... + if sys.platform != "win32": + def function_definition(self): ... + assignment = 1 + + def f2(self) -> str: ... \ No newline at end of file diff --git a/tests/test_format.py b/tests/test_format.py index 0650a2d6e53..f3db423b637 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -201,9 +201,9 @@ def test_stub() -> None: assert_format(source, expected, mode) -def test_nested_class_stub() -> None: +def test_nested_stub() -> None: mode = replace(DEFAULT_MODE, is_pyi=True, preview=True) - source, expected = read_data("miscellaneous", "nested_class_stub.pyi") + source, expected = read_data("miscellaneous", "nested_stub.pyi") assert_format(source, expected, mode) From b70b2c619671f0c6adc722742181bd2fa6e2a2f4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 8 Sep 2023 20:24:49 -0700 Subject: [PATCH 537/700] Prepare release 23.9.0 (#3863) --- CHANGES.md | 48 +++++++++++++-------- docs/contributing/release_process.md | 2 + docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b0fa5f8745e..3829526871e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,14 +14,10 @@ -- More concise formatting for dummy implementations (#3796) - ### Configuration -- Black now applies exclusion and ignore logic before resolving symlinks (#3846) - ### Packaging @@ -34,9 +30,6 @@ -- Avoid importing `IPython` if notebook cells do not contain magics (#3782) -- Improve caching by comparing file hashes as fallback for mtime and size. (#3821) - ### Output @@ -45,23 +38,45 @@ -- Fix an issue in `blackd` with single character input (#3558) - ### Integrations +### Documentation + + + +## 23.9.0 + +### Preview style + +- More concise formatting for dummy implementations (#3796) +- In stub files, add a blank line between a statement with a body (e.g an + `if sys.version_info > (3, x):`) and a function definition on the same level (#3862) +- Fix a bug whereby spaces were removed from walrus operators within subscript(#3823) + +### Configuration + +- Black now applies exclusion and ignore logic before resolving symlinks (#3846) + +### Performance + +- Avoid importing `IPython` if notebook cells do not contain magics (#3782) +- Improve caching by comparing file hashes as fallback for mtime and size (#3821) + +### _Blackd_ + +- Fix an issue in `blackd` with single character input (#3558) + +### Integrations + - Black now has an [official pre-commit mirror](https://github.com/psf/black-pre-commit-mirror). Swapping `https://github.com/psf/black` to `https://github.com/psf/black-pre-commit-mirror` in your `.pre-commit-config.yaml` will make Black about 2x faster (#3828) - The `.black.env` folder specified by `ENV_PATH` will now be removed on the completion - of the GitHub Action. (#3759) - -### Documentation - - + of the GitHub Action (#3759) ## 23.7.0 @@ -80,7 +95,6 @@ (#3740) - Fix error in AST validation when _Black_ removes trailing whitespace in a type comment (#3773) -- Fix a bug whereby spaces were removed from walrus operators within subscript (#3823) ### Preview style @@ -171,8 +185,6 @@ expected to become part of Black's stable style in January 2024. - For stubs, enforce one blank line after a nested class with a body other than just `...` (#3564) - Improve handling of multiline strings by changing line split behavior (#1879) -- In stub files, add a blank line between a statement with a body (e.g an - `if sys.version_info > (3, x):`) and a function definition on the same level. (#3862) ### Parser diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index be9b08a6c82..02865d6f4bd 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -43,6 +43,8 @@ To cut a release: 1. Remove any empty sections for the current release 1. (_optional_) Read through and copy-edit the changelog (eg. by moving entries, fixing typos, or rephrasing entries) + 1. Double-check that no changelog entries since the last release were put in the + wrong section (e.g., run `git diff CHANGES.md`) 1. Add a new empty template for the next release above ([template below](#changelog-template)) 1. Update references to the latest version in diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 24e732848f1..28414973ff5 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.7.0 + rev: 23.9.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.7.0 + rev: 23.9.0 hooks: - id: black-jupyter # 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 5efb50a9a12..6ae9441fb69 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -193,8 +193,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.7.0 (compiled: yes) -$ black --required-version 23.7.0 -c "format = 'this'" +black, 23.9.0 (compiled: yes) +$ black --required-version 23.9.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -285,7 +285,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.7.0 +black, 23.9.0 ``` #### `--config` From 716fa08090b6a51e4c72dd0a14b6c45f7da4a9d0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 8 Sep 2023 22:16:15 -0700 Subject: [PATCH 538/700] Upgrade mypy (#3864) --- CHANGES.md | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3829526871e..1334efefe7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- Upgrade to mypy 1.5.1 (#3864) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index ea5c9f84684..8585f4efbed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,7 @@ sources = ["src"] enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.3", + "mypy==1.5.1", "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true From 4e93f2aa01606154dc6af6e494b9f2b7e4c8c7fa Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:16:25 -0700 Subject: [PATCH 539/700] Add classifier for 3.12 (#3866) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8585f4efbed..ebfbede8559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", ] From add161b367a0d5a3cc395ec8e045f7b965edaef8 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 9 Sep 2023 12:08:28 -0400 Subject: [PATCH 540/700] Bump RTD Python version from 3.8 to 3.11 (#3868) Recent ReadTheDocs builds have been failing as our documentation dependencies (notably Sphinx) require Python 3.9+. --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index fff2d6ed341..fa612668850 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,7 +6,7 @@ formats: build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.11" python: install: From 4eebfd1a7a4aa2652cfc674cf301d1f2f48098aa Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 10 Sep 2023 07:53:27 -0700 Subject: [PATCH 541/700] Add mypyc test marks to new tests that patch (#3871) This is enough for me to get a clean test run on Python 3.9 with mypyc. I have not been able to repro the pickle failures on either Linux or macOS. --- tests/test_black.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_black.py b/tests/test_black.py index 4fb6aef9bca..badb8fff5fb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1985,6 +1985,7 @@ def test_cache_multiple_files(self) -> None: assert not cache.is_changed(one) assert not cache.is_changed(two) + @pytest.mark.incompatible_with_mypyc @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"]) def test_no_cache_when_writeback_diff(self, color: bool) -> None: mode = DEFAULT_MODE @@ -2046,6 +2047,7 @@ def test_write_cache_read_cache(self) -> None: read_cache = black.Cache.read(mode) assert not read_cache.is_changed(src) + @pytest.mark.incompatible_with_mypyc def test_filter_cached(self) -> None: with TemporaryDirectory() as workspace: path = Path(workspace) From c83ad6c077e7bb281cfd3fbdd89bbeb4c980e563 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 10 Sep 2023 15:36:25 -0600 Subject: [PATCH 542/700] Upgrade to Furo 2023.9.10 to fix docs build (#3873) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dad39f67ed3..b8bab4393ff 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ Sphinx==7.2.3 docutils==0.19 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.2 -furo==2023.8.19 +furo==2023.9.10 From 0b62b9c9a44a995e44d64ecf7cc08d8d7037642d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Sep 2023 15:45:13 -0700 Subject: [PATCH 543/700] Ignore aiohttp DeprecationWarning for 3.12 (#3876) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ebfbede8559..3d81e8f5ba9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -209,7 +209,10 @@ filterwarnings = [ # https://github.com/aio-libs/aiohttp/issues/6905 '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', # Work around https://github.com/pytest-dev/pytest/issues/10977 for Python 3.12 - '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''' + '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''', + # Will be fixed with aiohttp 3.9.0 + # https://github.com/aio-libs/aiohttp/pull/7302 + "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning", ] [tool.coverage.report] omit = [ From f7917453c99f8183ffd0397affcccb2c37594771 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Sep 2023 16:12:20 -0700 Subject: [PATCH 544/700] Re-export black.Mode (#3875) --- src/black/__init__.py | 11 +++-------- tests/test_black.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6fc91d2e6d3..188a4f79f0e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -63,14 +63,9 @@ ) from black.linegen import LN, LineGenerator, transform_line from black.lines import EmptyLineTracker, LinesBlock -from black.mode import ( - FUTURE_FLAG_TO_FEATURE, - VERSION_TO_FEATURES, - Feature, - Mode, - TargetVersion, - supports_feature, -) +from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature +from black.mode import Mode as Mode # re-exported +from black.mode import TargetVersion, supports_feature from black.nodes import ( STARS, is_number_token, diff --git a/tests/test_black.py b/tests/test_black.py index badb8fff5fb..d22b6859607 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2482,6 +2482,41 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: ) +class TestDeFactoAPI: + """Test that certain symbols that are commonly used externally keep working. + + We don't (yet) formally expose an API (see issue #779), but we should endeavor to + keep certain functions that external users commonly rely on working. + + """ + + def test_format_str(self) -> None: + # format_str and Mode should keep working + assert ( + black.format_str("print('hello')", mode=black.Mode()) == 'print("hello")\n' + ) + + # you can pass line length + assert ( + black.format_str("print('hello')", mode=black.Mode(line_length=42)) + == 'print("hello")\n' + ) + + # invalid input raises InvalidInput + with pytest.raises(black.InvalidInput): + black.format_str("syntax error", mode=black.Mode()) + + def test_format_file_contents(self) -> None: + # You probably should be using format_str() instead, but let's keep + # this one around since people do use it + assert ( + black.format_file_contents("x=1", fast=True, mode=black.Mode()) == "x = 1\n" + ) + + with pytest.raises(black.NothingChanged): + black.format_file_contents("x = 1\n", fast=True, mode=black.Mode()) + + try: with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 751583a1dfc691423d96d7711a4c8d9cfe3ee9c8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Sep 2023 16:16:24 -0700 Subject: [PATCH 545/700] Pickle raw tuples in FileData cache (#3877) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- CHANGES.md | 3 +++ src/black/cache.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1334efefe7b..9fa14f3ebc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,9 @@ +- Store raw tuples instead of NamedTuples in Black's cache, improving performance and + decreasing the size of the cache (#3877) + ### Output diff --git a/src/black/cache.py b/src/black/cache.py index ff15da2a94e..77f66cc34a9 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -67,7 +67,8 @@ def read(cls, mode: Mode) -> Self: with cache_file.open("rb") as fobj: try: - file_data: Dict[str, FileData] = pickle.load(fobj) + data: Dict[str, Tuple[float, int, str]] = pickle.load(fobj) + file_data = {k: FileData(*v) for k, v in data.items()} except (pickle.UnpicklingError, ValueError, IndexError): return cls(mode, cache_file) @@ -129,7 +130,12 @@ def write(self, sources: Iterable[Path]) -> None: with tempfile.NamedTemporaryFile( dir=str(self.cache_file.parent), delete=False ) as f: - pickle.dump(self.file_data, f, protocol=4) + # We store raw tuples in the cache because pickling NamedTuples + # doesn't work with mypyc on Python 3.8, and because it's faster. + data: Dict[str, Tuple[float, int, str]] = { + k: (*v,) for k, v in self.file_data.items() + } + pickle.dump(data, f, protocol=4) os.replace(f.name, self.cache_file) except OSError: pass From 62dca32dc55a850f39d78ba8b9c21cc4261a2ddf Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:47:08 -0700 Subject: [PATCH 546/700] mypyc builds on PRs, skip mypyc wheels for 3.12 (#3870) Co-authored-by: Jelle Zijlstra --- .github/workflows/pypi_upload.yml | 12 ++++++++---- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 9be231dd305..ab2c6402c23 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -1,8 +1,9 @@ -name: Publish to PyPI +name: Build wheels and publish to PyPI on: release: types: [published] + pull_request: permissions: contents: read @@ -28,7 +29,8 @@ jobs: - name: Build wheel and source distributions run: python -m build - - name: Upload to PyPI via Twine + - if: github.event_name == 'release' + name: Upload to PyPI via Twine env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: twine upload --verbose -u '__token__' dist/* @@ -68,7 +70,8 @@ jobs: name: ${{ matrix.name }}-mypyc-wheels path: ./wheelhouse/*.whl - - name: Upload wheels to PyPI via Twine + - if: github.event_name == 'release' + name: Upload wheels to PyPI via Twine env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: pipx run twine upload --verbose -u '__token__' wheelhouse/*.whl @@ -87,7 +90,8 @@ jobs: ref: stable fetch-depth: 0 - - name: Update stable branch to release tag & push + - if: github.event_name == 'release' + name: Update stable branch to release tag & push run: | git reset --hard ${{ github.event.release.tag_name }} git push diff --git a/pyproject.toml b/pyproject.toml index 3d81e8f5ba9..159907ac8ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,7 @@ build-verbosity = 1 # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS build = "cp3*-*" -skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp-*"] +skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp-*", "cp312-*"] # This is the bare minimum needed to run the test suite. Pulling in the full # test_requirements.txt would download a bunch of other packages not necessary # here and would slow down the testing step a fair bit. From e87737140f32d3cd7c44ede75f02dcd58e55820e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Sep 2023 17:35:41 -0700 Subject: [PATCH 547/700] Prepare release 23.9.1 (#3878) --- CHANGES.md | 23 ++++++++++++++++----- docs/integrations/source_version_control.md | 4 ++-- docs/usage_and_configuration/the_basics.md | 6 +++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9fa14f3ebc4..a68106ad23f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,8 +22,6 @@ -- Upgrade to mypy 1.5.1 (#3864) - ### Parser @@ -32,9 +30,6 @@ -- Store raw tuples instead of NamedTuples in Black's cache, improving performance and - decreasing the size of the cache (#3877) - ### Output @@ -52,6 +47,24 @@ +## 23.9.1 + +Due to various issues, the previous release (23.9.0) did not include compiled mypyc +wheels, which make Black significantly faster. These issues have now been fixed, and +this release should come with compiled wheels once again. + +There will be no wheels for Python 3.12 due to a bug in mypyc. We will provide 3.12 +wheels in a future release as soon as the mypyc bug is fixed. + +### Packaging + +- Upgrade to mypy 1.5.1 (#3864) + +### Performance + +- Store raw tuples instead of NamedTuples in Black's cache, improving performance and + decreasing the size of the cache (#3877) + ## 23.9.0 ### Preview style diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 28414973ff5..2afcc02f3cd 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.0 + rev: 23.9.1 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.0 + rev: 23.9.1 hooks: - id: black-jupyter # 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 6ae9441fb69..57fafd87654 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -193,8 +193,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.9.0 (compiled: yes) -$ black --required-version 23.9.0 -c "format = 'this'" +black, 23.9.1 (compiled: yes) +$ black --required-version 23.9.1 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -285,7 +285,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.9.0 +black, 23.9.1 ``` #### `--config` From 213cb655188fd56c548be3f0d9191c30595407ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 09:34:36 -0700 Subject: [PATCH 548/700] Bump actions/checkout from 3 to 4 (#3883) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/changelog.yml | 2 +- .github/workflows/diff_shades.yml | 4 ++-- .github/workflows/diff_shades_comment.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pypi_upload.yml | 6 +++--- .github/workflows/test.yml | 6 +++--- .github/workflows/upload_binary.yml | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index b3e1f0b9024..a1804597d7d 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Grep CHANGES.md for PR number if: contains(github.event.pull_request.labels.*.name, 'skip news') != true diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index d685ef9456d..637bd527eaa 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -19,7 +19,7 @@ jobs: matrix: ${{ steps.set-config.outputs.matrix }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "*" @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout this repository (full clone) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # The baseline revision could be rather old so a full clone is ideal. fetch-depth: 0 diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 22c293f91d2..b86bd93410e 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -12,7 +12,7 @@ jobs: comment: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "*" diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fc94dea62d9..fa3d87c70f5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up latest Python uses: actions/setup-python@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8baace940ba..566fc880785 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v2 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4439148a1c7..1b5a50c0e0b 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -25,7 +25,7 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 064d4745a53..3eaf5785f5a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Assert PR target is main if: github.event_name == 'pull_request' && github.repository == 'psf/black' diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index ab2c6402c23..bf4d8349c95 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up latest Python uses: actions/setup-python@v4 @@ -57,7 +57,7 @@ jobs: macos_arch: "universal2" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build wheels via cibuildwheel uses: pypa/cibuildwheel@v2.15.0 @@ -85,7 +85,7 @@ jobs: steps: - name: Checkout stable branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: stable fetch-depth: 0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7daa31ee903..1f33f2b814f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -75,7 +75,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Send finished signal to Coveralls uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99 with: @@ -93,7 +93,7 @@ jobs: os: [ubuntu-latest, macOS-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up latest Python uses: actions/setup-python@v4 diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 22535a64c67..bb19d48158c 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -29,7 +29,7 @@ jobs: executable_mime: "application/x-mach-binary" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up latest Python uses: actions/setup-python@v4 From e73662ca7cfd6d4760e11a6ab489a1ec585d1cd4 Mon Sep 17 00:00:00 2001 From: Simon Alinder <92031780+AlinderS@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:47:47 +0200 Subject: [PATCH 549/700] Fix broken url in editors.md (#3885) * Fix broken url in editors.md Update a link pointing to the Arch Linux repos. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/integrations/editors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index cebe2b0721e..ab8c6a743e5 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -288,8 +288,8 @@ $ git checkout origin/stable -b stable ##### Arch Linux On Arch Linux, the plugin is shipped with the -[`python-black`](https://archlinux.org/packages/community/any/python-black/) package, so -you can start using it in Vim after install with no additional setup. +[`python-black`](https://archlinux.org/packages/extra/any/python-black/) package, so you +can start using it in Vim after install with no additional setup. ##### Vim 8 Native Plugin Management From b2f03f913282b359d6301c426093b59b04303cff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:19:57 -0700 Subject: [PATCH 550/700] Bump sphinx from 7.2.3 to 7.2.5 in /docs (#3882) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.2.3 to 7.2.5. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.2.3...v7.2.5) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index b8bab4393ff..e4471b76031 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==2.0.0 -Sphinx==7.2.3 +Sphinx==7.2.5 # Older versions break Sphinx even though they're declared to be supported. docutils==0.19 sphinxcontrib-programoutput==0.17 From 14f60c84c84d6f872c09ec2171a873cac75f4c0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:20:36 -0700 Subject: [PATCH 551/700] Bump docutils from 0.19 to 0.20.1 in /docs (#3699) Bumps [docutils](https://docutils.sourceforge.io/) from 0.19 to 0.20.1. --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e4471b76031..a29e295ae49 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ myst-parser==2.0.0 Sphinx==7.2.5 # Older versions break Sphinx even though they're declared to be supported. -docutils==0.19 +docutils==0.20.1 sphinxcontrib-programoutput==0.17 sphinx_copybutton==0.5.2 furo==2023.9.10 From 004fb79706a02c9a06abd5c416b033340f99e558 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:36:37 -0700 Subject: [PATCH 552/700] mypyc build improvements (#3881) Build in separate jobs. This makes it clearer if e.g. a single Python version is failing. It also potentially gets you more parallelism. Build everything on push to master. Only build Linux 3.8 and 3.11 wheels on PRs. --- .github/workflows/pypi_upload.yml | 71 ++++++++++++++++++++++--------- pyproject.toml | 4 +- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index bf4d8349c95..813ac39186f 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -1,9 +1,12 @@ -name: Build wheels and publish to PyPI +name: Build and publish on: release: types: [published] pull_request: + push: + branches: + - main permissions: contents: read @@ -12,6 +15,7 @@ jobs: main: name: sdist + pure wheel runs-on: ubuntu-latest + if: github.event_name == 'release' steps: - uses: actions/checkout@v4 @@ -35,34 +39,58 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: twine upload --verbose -u '__token__' dist/* + generate_wheels_matrix: + name: generate wheels matrix + runs-on: ubuntu-latest + outputs: + include: ${{ steps.set-matrix.outputs.include }} + steps: + - uses: actions/checkout@v3 + - name: Install cibuildwheel and pypyp + run: | + pipx install cibuildwheel==2.15.0 + pipx install pypyp==1 + - name: generate matrix + if: github.event_name != 'pull_request' + run: | + { + cibuildwheel --print-build-identifiers --platform linux \ + | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' \ + && cibuildwheel --print-build-identifiers --platform macos \ + | pyp 'json.dumps({"only": x, "os": "macos-latest"})' \ + && cibuildwheel --print-build-identifiers --platform windows \ + | pyp 'json.dumps({"only": x, "os": "windows-latest"})' + } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix + env: + CIBW_ARCHS_LINUX: x86_64 + CIBW_ARCHS_MACOS: x86_64 arm64 + CIBW_ARCHS_WINDOWS: AMD64 + - name: generate matrix (PR) + if: github.event_name == 'pull_request' + run: | + cibuildwheel --print-build-identifiers --platform linux \ + | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' \ + | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix + env: + CIBW_BUILD: "cp38-* cp311-*" + CIBW_ARCHS_LINUX: x86_64 + - id: set-matrix + run: echo "include=$(cat /tmp/matrix)" | tee -a $GITHUB_OUTPUT + mypyc: - name: mypyc wheels (${{ matrix.name }}) + name: mypyc wheels ${{ matrix.only }} + needs: generate_wheels_matrix runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - name: linux-x86_64 - - os: windows-2019 - name: windows-amd64 - - os: macos-11 - name: macos-x86_64 - macos_arch: "x86_64" - - os: macos-11 - name: macos-arm64 - macos_arch: "arm64" - - os: macos-11 - name: macos-universal2 - macos_arch: "universal2" + include: ${{ fromJson(needs.generate_wheels_matrix.outputs.include) }} steps: - uses: actions/checkout@v4 - - - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v2.15.0 - env: - CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}" + - uses: pypa/cibuildwheel@v2.15.0 + with: + only: ${{ matrix.only }} - name: Upload wheels as workflow artifacts uses: actions/upload-artifact@v3 @@ -80,6 +108,7 @@ jobs: name: Update stable branch needs: [main, mypyc] runs-on: ubuntu-latest + if: github.event_name == 'release' permissions: contents: write diff --git a/pyproject.toml b/pyproject.toml index 159907ac8ec..d246eb0b272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,8 +145,8 @@ build-verbosity = 1 # - Python: CPython 3.8+ only # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS -build = "cp3*-*" -skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp-*", "cp312-*"] +build = "cp3*" +skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*", "cp312-*"] # This is the bare minimum needed to run the test suite. Pulling in the full # test_requirements.txt would download a bunch of other packages not necessary # here and would slow down the testing step a fair bit. From e9356c1ff0083aea4416bf1d3e29748634bb4f7f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:40:41 -0700 Subject: [PATCH 553/700] Document disabling E704 (#3888) Linking #3887 --- .flake8 | 2 +- docs/guides/using_black_with_other_tools.md | 2 +- docs/the_black_code_style/current_style.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.flake8 b/.flake8 index 7bc346a09c1..85f51cf9f05 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # B905 should be enabled when we drop support for 3.9 -ignore = E203, E266, E501, W503, B905, B907 +ignore = E203, E266, E501, E704, W503, B905, B907 # line length is intentionally set to 80 here because black uses Bugbear # See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details max-line-length = 80 diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 6c6fbb88174..22c641a7420 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -173,7 +173,7 @@ limit of `88`, _Black_'s default. This explains `max-line-length = 88`. ```ini [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ```
diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index e1a8078bf2c..ff757a8276b 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -173,7 +173,7 @@ If you use Flake8, you have a few options: max-line-length = 80 ... select = C,E,F,W,B,B950 - extend-ignore = E203, E501 + extend-ignore = E203, E501, E704 ``` The rationale for E950 is explained in @@ -184,7 +184,7 @@ If you use Flake8, you have a few options: ```ini [flake8] max-line-length = 88 - extend-ignore = E203 + extend-ignore = E203, E704 ``` An explanation of why E203 is disabled can be found in the [Slices section](#slices) of From 5a0615a7edd3339718a346577f03cf07da364025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:47:02 -0700 Subject: [PATCH 554/700] Bump sphinx from 7.2.5 to 7.2.6 in /docs (#3895) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.2.5 to 7.2.6. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.2.5...v7.2.6) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... 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 a29e295ae49..b5b9e22fc84 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Used by ReadTheDocs; pinned requirements for stability. myst-parser==2.0.0 -Sphinx==7.2.5 +Sphinx==7.2.6 # Older versions break Sphinx even though they're declared to be supported. docutils==0.20.1 sphinxcontrib-programoutput==0.17 From 34ed4cf8fd14eb423e3eb0fabf558aee93868a35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:47:26 -0700 Subject: [PATCH 555/700] Bump docker/build-push-action from 4 to 5 (#3894) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 566fc880785..41f92b58853 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,7 +36,7 @@ jobs: latest_non_release)" >> $GITHUB_ENV - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name == 'release' && github.event.action == 'published' && !github.event.release.prerelease }} - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 @@ -58,7 +58,7 @@ jobs: if: ${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease }} - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 From ab92daf408727718849d16fcd13590006e52c1bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:47:43 -0700 Subject: [PATCH 556/700] Bump docker/login-action from 2 to 3 (#3891) Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 41f92b58853..a5992f758c1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,7 +25,7 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From edf66baa21d337ae069be3871e95f88b68a3ffcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:48:03 -0700 Subject: [PATCH 557/700] Bump docker/setup-buildx-action from 2 to 3 (#3892) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a5992f758c1..80930ccfee8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 From f5990e85474f7641717e70bafb797462995d974f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:48:11 -0700 Subject: [PATCH 558/700] Bump docker/setup-qemu-action from 2 to 3 (#3890) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 80930ccfee8..ee858236fcf 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 7316a793187eedd94c288f1df648ecca0d8763dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:48:27 -0700 Subject: [PATCH 559/700] Bump actions/checkout from 3 to 4 (#3893) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 813ac39186f..2a74b7c6a55 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -45,7 +45,7 @@ jobs: outputs: include: ${{ steps.set-matrix.outputs.include }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install cibuildwheel and pypyp run: | pipx install cibuildwheel==2.15.0 From e974fc3c52959e9f01bf62bdcd0d8c100ce78985 Mon Sep 17 00:00:00 2001 From: Eero Vaher Date: Mon, 18 Sep 2023 20:35:07 +0300 Subject: [PATCH 560/700] Remove outdated mentions of runtime support of Python 3.7 (#3896) Remove mentions of runtime support of Python 3.7 Runtime support of Python 3.7 was removed in b4dca26c7d93f930bbd5a7b552807370b60d4298 but a few mentions of it being supported have remained until now. --- README.md | 2 +- autoload/black.vim | 4 ++-- docs/faq.md | 6 +++--- docs/getting_started.md | 2 +- docs/integrations/editors.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b257c333f0d..cad8184f7bc 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the ### Installation -_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. +_Black_ can be installed by running `pip install black`. It requires Python 3.8+ to run. If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/autoload/black.vim b/autoload/black.vim index 4eb9b25db28..051fea05c3b 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -75,8 +75,8 @@ def _initialize_black_env(upgrade=False): return True pyver = sys.version_info[:3] - if pyver < (3, 7): - print("Sorry, Black requires Python 3.7+ to run.") + if pyver < (3, 8): + print("Sorry, Black requires Python 3.8+ to run.") return False from pathlib import Path diff --git a/docs/faq.md b/docs/faq.md index 8941ca3fe4d..c62e1b504b5 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -86,7 +86,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Which Python versions does Black support? -Currently the runtime requires Python 3.7-3.11. Formatting is supported for files +Currently the runtime requires Python 3.8-3.11. Formatting is supported for files containing syntax from Python 3.3 to 3.11. 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. @@ -95,7 +95,7 @@ Support for formatting Python 2 code was removed in version 22.0. While we've ma 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. -Runtime support for 3.6 was removed in version 22.10.0. +Runtime support for 3.7 was removed in version 23.7.0. ## Why does my linter or typechecker complain after I format my code? @@ -107,7 +107,7 @@ codebase with _Black_. ## Can I run Black with PyPy? -Yes, there is support for PyPy 3.7 and higher. +Yes, there is support for PyPy 3.8 and higher. ## Why does Black not detect syntax errors in my code? diff --git a/docs/getting_started.md b/docs/getting_started.md index 33fb2f978bb..15b7646a509 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -16,7 +16,7 @@ Also, you can try out _Black_ online for minimal fuss on the ## Installation -_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run. +_Black_ can be installed by running `pip install black`. It requires Python 3.8+ to run. If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`. If you can't wait for the latest _hotness_ and want to install from GitHub, use: diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index ab8c6a743e5..9c7cfe19083 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -236,7 +236,7 @@ Configuration: #### Installation -This plugin **requires Vim 7.0+ built with Python 3.7+ support**. It needs Python 3.7 to +This plugin **requires Vim 7.0+ built with Python 3.8+ support**. It needs Python 3.8 to be able to run _Black_ inside the Vim process which is much faster than calling an external command. From 8c5d96ffd3da6d621e67639dadd26a1d7d0227cd Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:38:51 +0200 Subject: [PATCH 561/700] fix indentation of line breaks in long type hints by adding parens (#3899) * fix indentation of line breaks in long type hints by adding parentheses, and remove unnecessary parentheses * add entry in CHANGES.md, make the style change only in preview mode --- CHANGES.md | 3 + src/black/linegen.py | 30 ++- src/black/mode.py | 1 + .../preview/long_strings__type_annotations.py | 2 +- .../pep604_union_types_line_breaks.py | 187 ++++++++++++++++++ 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 tests/data/preview_py_310/pep604_union_types_line_breaks.py diff --git a/CHANGES.md b/CHANGES.md index a68106ad23f..a879ab3e8da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,9 @@ ### Preview style +- Long type hints are now wrapped in parentheses and properly indented when split across + multiple lines (#3899) + ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 507e860190f..9ddd4619f69 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -397,6 +397,24 @@ def visit_factor(self, node: Node) -> Iterator[Line]: node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) yield from self.visit_default(node) + def visit_tname(self, node: Node) -> Iterator[Line]: + """ + Add potential parentheses around types in function parameter lists to be made + into real parentheses in case the type hint is too long to fit on a line + Examples: + def foo(a: int, b: float = 7): ... + + -> + + def foo(a: (int), b: (float) = 7): ... + """ + if Preview.parenthesize_long_type_hints in self.mode: + assert len(node.children) == 3 + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + wrap_in_parentheses(node, node.children[2], visible=False) + + yield from self.visit_default(node) + def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) @@ -498,7 +516,14 @@ def __post_init__(self) -> None: self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"}) self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) - self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) + + # When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py + if Preview.parenthesize_long_type_hints in self.mode: + assignments = ASSIGNMENTS | {":"} + else: + assignments = ASSIGNMENTS + self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments) + self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) @@ -1368,7 +1393,7 @@ def maybe_make_parens_invisible_in_atom( Returns whether the node should itself be wrapped in invisible parentheses. """ if ( - node.type != syms.atom + node.type not in (syms.atom, syms.expr) or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) @@ -1392,6 +1417,7 @@ def maybe_make_parens_invisible_in_atom( syms.except_clause, syms.funcdef, syms.with_stmt, + syms.tname, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, diff --git a/src/black/mode.py b/src/black/mode.py index 8a855ac495a..f44a821bcd0 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -180,6 +180,7 @@ class Preview(Enum): # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() parenthesize_conditional_expressions = auto() + parenthesize_long_type_hints = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() diff --git a/tests/data/preview/long_strings__type_annotations.py b/tests/data/preview/long_strings__type_annotations.py index 41d7ee2b67b..45de882d02c 100644 --- a/tests/data/preview/long_strings__type_annotations.py +++ b/tests/data/preview/long_strings__type_annotations.py @@ -54,6 +54,6 @@ def func( def func( - argument: ("int |" "str"), + argument: "int |" "str", ) -> Set["int |" " str"]: pass diff --git a/tests/data/preview_py_310/pep604_union_types_line_breaks.py b/tests/data/preview_py_310/pep604_union_types_line_breaks.py new file mode 100644 index 00000000000..9c4ab870766 --- /dev/null +++ b/tests/data/preview_py_310/pep604_union_types_line_breaks.py @@ -0,0 +1,187 @@ +# This has always worked +z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong + +# "AnnAssign"s now also work +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong +z: (Short + | Short2 + | Short3 + | Short4) +z: (int) +z: ((int)) + + +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 +z: (Short + | Short2 + | Short3 + | Short4) = 8 +z: (int) = 2.3 +z: ((int)) = foo() + +# In case I go for not enforcing parantheses, this might get improved at the same time +x = ( + z + == 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999, + y + == 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999, +) + +x = ( + z == (9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999), + y == (9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999), +) + +# handle formatting of "tname"s in parameter list + +# remove unnecessary paren +def foo(i: (int)) -> None: ... + + +# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. +def foo(i: (int,)) -> None: ... + +def foo( + i: int, + x: Loooooooooooooooooooooooong + | Looooooooooooooooong + | Looooooooooooooooooooong + | Looooooong, + *, + s: str, +) -> None: + pass + + +@app.get("/path/") +async def foo( + q: str + | None = Query(None, title="Some long title", description="Some long description") +): + pass + + +def f( + max_jobs: int + | None = Option( + None, help="Maximum number of jobs to launch. And some additional text." + ), + another_option: bool = False + ): + ... + + +# output +# This has always worked +z = ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) + +# "AnnAssign"s now also work +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) +z: Short | Short2 | Short3 | Short4 +z: int +z: int + + +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) = 7 +z: Short | Short2 | Short3 | Short4 = 8 +z: int = 2.3 +z: int = foo() + +# In case I go for not enforcing parantheses, this might get improved at the same time +x = ( + z + == 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999, + y + == 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999, +) + +x = ( + z + == ( + 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + ), + y + == ( + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + ), +) + +# handle formatting of "tname"s in parameter list + + +# remove unnecessary paren +def foo(i: int) -> None: ... + + +# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. +def foo(i: (int,)) -> None: ... + + +def foo( + i: int, + x: ( + Loooooooooooooooooooooooong + | Looooooooooooooooong + | Looooooooooooooooooooong + | Looooooong + ), + *, + s: str, +) -> None: + pass + + +@app.get("/path/") +async def foo( + q: str | None = Query( + None, title="Some long title", description="Some long description" + ) +): + pass + + +def f( + max_jobs: int | None = Option( + None, help="Maximum number of jobs to launch. And some additional text." + ), + another_option: bool = False, +): ... From 5f6ea5ff20100290ba8e8803a924caea12d2d0b6 Mon Sep 17 00:00:00 2001 From: Syed Mohammad Ibrahim <8592115+iamibi@users.noreply.github.com> Date: Sun, 24 Sep 2023 07:53:03 +0530 Subject: [PATCH 562/700] added the py311 to target-version config (#3898) --- docs/usage_and_configuration/the_basics.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 57fafd87654..36119f225e6 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -52,18 +52,19 @@ See also [the style documentation](labels/line-length). #### `-t`, `--target-version` -Python versions that should be supported by Black's output. You should include all -versions that your code supports. If you support Python 3.7 through 3.10, you should -write: +Python versions that should be supported by Black's output. You can run `black --help` +and look for the `--target-version` option to see the full list of supported versions. +You should include all versions that your code supports. If you support Python 3.8 +through 3.11, you should write: ```console -$ black -t py37 -t py38 -t py39 -t py310 +$ black -t py38 -t py39 -t py310 -t py311 ``` In a [configuration file](#configuration-via-a-file), you can write: ```toml -target-version = ["py37", "py38", "py39", "py310"] +target-version = ["py38", "py39", "py310", "py311"] ``` _Black_ uses this option to decide what grammar to use to parse your code. In addition, From 3dcacdda0d7f69a1705f3e2a151c24a6cf004171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:32:58 -0700 Subject: [PATCH 563/700] Bump pypa/cibuildwheel from 2.15.0 to 2.16.0 (#3901) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.15.0 to 2.16.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.15.0...v2.16.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel 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> --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 2a74b7c6a55..026f74e1c9f 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pypa/cibuildwheel@v2.15.0 + - uses: pypa/cibuildwheel@v2.16.0 with: only: ${{ matrix.only }} From 9b82120ddb81373377b58da5de7caa9495a1551e Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:03:24 +0200 Subject: [PATCH 564/700] add support for printing the diff of AST trees when running tests (#3902) Co-authored-by: Jelle Zijlstra --- docs/contributing/the_basics.md | 38 +++++++++++++++++++++++++++++++++ src/black/debug.py | 21 ++++++++++++------ tests/conftest.py | 27 +++++++++++++++++++++++ tests/test_black.py | 29 ++++++++++++++++++++++--- tests/util.py | 24 ++++++++++++++++----- tox.ini | 2 +- 6 files changed, 125 insertions(+), 16 deletions(-) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 40d233257e3..864894b491f 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -37,6 +37,44 @@ the root of the black repo: (.venv)$ tox -e run_self ``` +### Development + +Further examples of invoking the tests + +```console +# Run all of the above mentioned, in parallel +(.venv)$ tox --parallel=auto + +# Run tests on a specific python version +(.venv)$ tox -e py39 + +# pass arguments to pytest +(.venv)$ tox -e py -- --no-cov + +# print full tree diff, see documentation below +(.venv)$ tox -e py -- --print-full-tree + +# disable diff printing, see documentation below +(.venv)$ tox -e py -- --print-tree-diff=False +``` + +`Black` has two pytest command-line options affecting test files in `tests/data/` that +are split into an input part, and an output part, separated by a line with`# output`. +These can be passed to `pytest` through `tox`, or directly into pytest if not using +`tox`. + +#### `--print-full-tree` + +Upon a failing test, print the full concrete syntax tree (CST) as it is after processing +the input ("actual"), and the tree that's yielded after parsing the output ("expected"). +Note that a test can fail with different output with the same CST. This used to be the +default, but now defaults to `False`. + +#### `--print-tree-diff` + +Upon a failing test, print the diff of the trees as described above. This is the +default. To turn it off pass `--print-tree-diff=False`. + ### News / Changelog Requirement `Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If diff --git a/src/black/debug.py b/src/black/debug.py index 150b44842dd..cebc48765ba 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass -from typing import Iterator, TypeVar, Union +from dataclasses import dataclass, field +from typing import Any, Iterator, List, TypeVar, Union from black.nodes import Visitor from black.output import out @@ -14,26 +14,33 @@ @dataclass class DebugVisitor(Visitor[T]): tree_depth: int = 0 + list_output: List[str] = field(default_factory=list) + print_output: bool = True + + def out(self, message: str, *args: Any, **kwargs: Any) -> None: + self.list_output.append(message) + if self.print_output: + out(message, *args, **kwargs) def visit_default(self, node: LN) -> Iterator[T]: indent = " " * (2 * self.tree_depth) if isinstance(node, Node): _type = type_repr(node.type) - out(f"{indent}{_type}", fg="yellow") + self.out(f"{indent}{_type}", fg="yellow") self.tree_depth += 1 for child in node.children: yield from self.visit(child) self.tree_depth -= 1 - out(f"{indent}/{_type}", fg="yellow", bold=False) + self.out(f"{indent}/{_type}", fg="yellow", bold=False) else: _type = token.tok_name.get(node.type, str(node.type)) - out(f"{indent}{_type}", fg="blue", nl=False) + self.out(f"{indent}{_type}", fg="blue", nl=False) if node.prefix: # We don't have to handle prefixes for `Node` objects since # that delegates to the first child anyway. - out(f" {node.prefix!r}", fg="green", bold=False, nl=False) - out(f" {node.value!r}", fg="blue", bold=False) + self.out(f" {node.prefix!r}", fg="green", bold=False, nl=False) + self.out(f" {node.value!r}", fg="blue", bold=False) @classmethod def show(cls, code: Union[str, Leaf, Node]) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 67517268d1b..1a0dd747d8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +1,28 @@ +import pytest + pytest_plugins = ["tests.optional"] + +PRINT_FULL_TREE: bool = False +PRINT_TREE_DIFF: bool = True + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption( + "--print-full-tree", + action="store_true", + default=False, + help="print full syntax trees on failed tests", + ) + parser.addoption( + "--print-tree-diff", + action="store_true", + default=True, + help="print diff of syntax trees on failed tests", + ) + + +def pytest_configure(config: pytest.Config) -> None: + global PRINT_FULL_TREE + global PRINT_TREE_DIFF + PRINT_FULL_TREE = config.getoption("--print-full-tree") + PRINT_TREE_DIFF = config.getoption("--print-tree-diff") diff --git a/tests/test_black.py b/tests/test_black.py index d22b6859607..c665eee3a6c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -9,7 +9,6 @@ import re import sys import types -import unittest from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager, redirect_stderr from dataclasses import replace @@ -1047,9 +1046,10 @@ def test_endmarker(self) -> None: self.assertEqual(len(n.children), 1) self.assertEqual(n.children[0].type, black.token.ENDMARKER) + @patch("tests.conftest.PRINT_FULL_TREE", True) + @patch("tests.conftest.PRINT_TREE_DIFF", False) @pytest.mark.incompatible_with_mypyc - @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT") - def test_assertFormatEqual(self) -> None: + def test_assertFormatEqual_print_full_tree(self) -> None: out_lines = [] err_lines = [] @@ -1068,6 +1068,29 @@ def err(msg: str, **kwargs: Any) -> None: self.assertIn("Actual tree:", out_str) self.assertEqual("".join(err_lines), "") + @patch("tests.conftest.PRINT_FULL_TREE", False) + @patch("tests.conftest.PRINT_TREE_DIFF", True) + @pytest.mark.incompatible_with_mypyc + def test_assertFormatEqual_print_tree_diff(self) -> None: + out_lines = [] + err_lines = [] + + def out(msg: str, **kwargs: Any) -> None: + out_lines.append(msg) + + def err(msg: str, **kwargs: Any) -> None: + err_lines.append(msg) + + with patch("black.output._out", out), patch("black.output._err", err): + with self.assertRaises(AssertionError): + self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n") + + out_str = "".join(out_lines) + self.assertIn("Tree Diff:", out_str) + self.assertIn("+ COMMA", out_str) + self.assertIn("+ ','", out_str) + self.assertEqual("".join(err_lines), "") + @event_loop() @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError)) def test_works_in_mono_process_only_environment(self) -> None: diff --git a/tests/util.py b/tests/util.py index 967d576fafe..541d21da4df 100644 --- a/tests/util.py +++ b/tests/util.py @@ -12,6 +12,8 @@ from black.mode import TargetVersion from black.output import diff, err, out +from . import conftest + PYTHON_SUFFIX = ".py" ALLOWED_SUFFIXES = (PYTHON_SUFFIX, ".pyi", ".out", ".diff", ".ipynb") @@ -34,22 +36,34 @@ def _assert_format_equal(expected: str, actual: str) -> None: - if actual != expected and not os.environ.get("SKIP_AST_PRINT"): + if actual != expected and (conftest.PRINT_FULL_TREE or conftest.PRINT_TREE_DIFF): bdv: DebugVisitor[Any] - out("Expected tree:", fg="green") + actual_out: str = "" + expected_out: str = "" + if conftest.PRINT_FULL_TREE: + out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) - bdv = DebugVisitor() + bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE) list(bdv.visit(exp_node)) + expected_out = "\n".join(bdv.list_output) except Exception as ve: err(str(ve)) - out("Actual tree:", fg="red") + if conftest.PRINT_FULL_TREE: + out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) - bdv = DebugVisitor() + bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE) list(bdv.visit(exp_node)) + actual_out = "\n".join(bdv.list_output) except Exception as ve: err(str(ve)) + if conftest.PRINT_TREE_DIFF: + out("Tree Diff:") + out( + diff(expected_out, actual_out, "expected tree", "actual tree") + or "Trees do not differ" + ) if actual != expected: out(diff(expected, actual, "expected", "actual")) diff --git a/tox.ini b/tox.ini index d34dbbc71db..018cef993c0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = true -envlist = {,ci-}py{37,38,39,310,311,py3},fuzz,run_self +envlist = {,ci-}py{38,39,310,311,py3},fuzz,run_self [testenv] setenv = From e7c3368c1316c38338cef34fffc42ea3252b1802 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 28 Sep 2023 09:10:01 -0700 Subject: [PATCH 565/700] Try newer clang in diff-shades job (#3904) --- .github/workflows/diff_shades.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 637bd527eaa..97db907abc8 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -44,7 +44,7 @@ jobs: HATCH_BUILD_HOOKS_ENABLE: "1" # Clang is less picky with the C code it's given than gcc (and may # generate faster binaries too). - CC: clang-12 + CC: clang-14 strategy: fail-fast: false matrix: From a91eb73064c9bef76c3a961ab662bb5f75a1543d Mon Sep 17 00:00:00 2001 From: Eddie Darling Date: Sun, 1 Oct 2023 15:35:42 -0700 Subject: [PATCH 566/700] Fix comments getting removed from inside parenthesized strings (#3909) Since the id of the old leaf may be the key to comments, the new leaf must adopt the old comments --- CHANGES.md | 2 ++ src/black/trans.py | 3 +++ tests/data/preview/comments7.py | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index a879ab3e8da..028a01acd62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ +- Fix comments getting removed from inside parenthesized strings (#3909) + ### Configuration diff --git a/src/black/trans.py b/src/black/trans.py index daed26427d7..c0cc92613ac 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -942,6 +942,9 @@ def _transform_to_new_line( LL[lpar_or_rpar_idx].remove() # Remove lpar. replace_child(LL[idx], string_leaf) new_line.append(string_leaf) + # replace comments + old_comments = new_line.comments.pop(id(LL[idx]), []) + new_line.comments.setdefault(id(string_leaf), []).extend(old_comments) else: LL[lpar_or_rpar_idx].remove() # This is a rpar. diff --git a/tests/data/preview/comments7.py b/tests/data/preview/comments7.py index 8b1224017e5..0655de999ec 100644 --- a/tests/data/preview/comments7.py +++ b/tests/data/preview/comments7.py @@ -131,6 +131,18 @@ def test_fails_invalid_post_data( square = Square(4) # type: Optional[Square] +# Regression test for https://github.com/psf/black/issues/3756. +[ + ( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ), +] +[ + ( # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ), +] + # output from .config import ( @@ -282,3 +294,15 @@ def test_fails_invalid_post_data( square = Square(4) # type: Optional[Square] + +# Regression test for https://github.com/psf/black/issues/3756. +[ + ( # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ), +] +[ + ( # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ), +] From f99ef6e190785b3e6a58e83f1382e7d6d3c4881e Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 1 Oct 2023 15:41:32 -0700 Subject: [PATCH 567/700] Fix up changelog (#3910) --- CHANGES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 028a01acd62..5e518497c92 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,14 +10,14 @@ -### Preview style +- Fix comments getting removed from inside parenthesized strings (#3909) -- Long type hints are now wrapped in parentheses and properly indented when split across - multiple lines (#3899) +### Preview style -- Fix comments getting removed from inside parenthesized strings (#3909) +- Long type hints are now wrapped in parentheses and properly indented when split across + multiple lines (#3899) ### Configuration From 1b08cbc63400a8b8e0fbb620b6b2a4dab35e1e7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 23:40:00 -0700 Subject: [PATCH 568/700] Bump pypa/cibuildwheel from 2.16.0 to 2.16.1 (#3911) --- .github/workflows/pypi_upload.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 026f74e1c9f..41ab6460793 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pypa/cibuildwheel@v2.16.0 + - uses: pypa/cibuildwheel@v2.16.1 with: only: ${{ matrix.only }} From 9e9fdce9a81a53fd3771e1825de523a6413b3c35 Mon Sep 17 00:00:00 2001 From: Shreya Agarwal Date: Mon, 2 Oct 2023 20:05:57 +0530 Subject: [PATCH 569/700] docs: use LSP for SublimeText 4 (#3913) --- docs/integrations/editors.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 9c7cfe19083..83904144c48 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -399,9 +399,10 @@ close and reopen your File, _Black_ will be done with its job. server for Black. Formatting is much more responsive using this extension, **but the minimum supported version of Black is 22.3.0**. -## SublimeText 3 +## SublimeText -Use [sublack plugin](https://github.com/jgirardet/sublack). +For SublimeText 3, use [sublack plugin](https://github.com/jgirardet/sublack). For +higher versions, it is recommended to use [LSP](#python-lsp-server) as documented below. ## Python LSP Server From 947bd3825e5dc67f16f48f916462c4470b7a5247 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:19:53 -0700 Subject: [PATCH 570/700] [pre-commit.ci] pre-commit autoupdate (#3915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1) - [github.com/pre-commit/mirrors-prettier: v3.0.1 → v3.0.3](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.1...v3.0.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6301526a445..99b3565ed0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.0 + rev: v1.5.1 hooks: - id: mypy exclude: ^docs/conf.py @@ -53,7 +53,7 @@ repos: - hypothesis - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.1 + rev: v3.0.3 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml From 36078bc83f24dcd5f74e021a105429595a3fd63c Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 5 Oct 2023 01:42:35 +0200 Subject: [PATCH 571/700] respect magic trailing commas in return types (#3916) --- CHANGES.md | 1 + src/black/linegen.py | 36 ++- src/black/mode.py | 1 + .../return_annotation_brackets_string.py | 11 + .../funcdef_return_type_trailing_comma.py | 300 ++++++++++++++++++ .../return_annotation_brackets.py | 13 + 6 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 tests/data/preview_py_310/funcdef_return_type_trailing_comma.py diff --git a/CHANGES.md b/CHANGES.md index 5e518497c92..888824ee055 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,7 @@ - Long type hints are now wrapped in parentheses and properly indented when split across multiple lines (#3899) +- Magic trailing commas are now respected in return types. (#3916) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 9ddd4619f69..bdc4ee54ab2 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -573,7 +573,7 @@ def transform_line( transformers = [string_merge, string_paren_strip] else: transformers = [] - elif line.is_def: + elif line.is_def and not should_split_funcdef_with_rhs(line, mode): transformers = [left_hand_split] else: @@ -652,6 +652,40 @@ def _rhs( yield line +def should_split_funcdef_with_rhs(line: Line, mode: Mode) -> bool: + """If a funcdef has a magic trailing comma in the return type, then we should first + split the line with rhs to respect the comma. + """ + if Preview.respect_magic_trailing_comma_in_return_type not in mode: + return False + + return_type_leaves: List[Leaf] = [] + in_return_type = False + + for leaf in line.leaves: + if leaf.type == token.COLON: + in_return_type = False + if in_return_type: + return_type_leaves.append(leaf) + if leaf.type == token.RARROW: + in_return_type = True + + # using `bracket_split_build_line` will mess with whitespace, so we duplicate a + # couple lines from it. + result = Line(mode=line.mode, depth=line.depth) + leaves_to_track = get_leaves_inside_matching_brackets(return_type_leaves) + for leaf in return_type_leaves: + result.append( + leaf, + preformatted=True, + track_bracket=id(leaf) in leaves_to_track, + ) + + # we could also return true if the line is too long, and the return type is longer + # than the param list. Or if `should_split_rhs` returns True. + return result.magic_trailing_comma is not None + + class _BracketSplitComponent(Enum): head = auto() body = auto() diff --git a/src/black/mode.py b/src/black/mode.py index f44a821bcd0..30c5d2f1b2f 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -181,6 +181,7 @@ class Preview(Enum): string_processing = auto() parenthesize_conditional_expressions = auto() parenthesize_long_type_hints = auto() + respect_magic_trailing_comma_in_return_type = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() diff --git a/tests/data/preview/return_annotation_brackets_string.py b/tests/data/preview/return_annotation_brackets_string.py index 6978829fd5c..9148bd045bc 100644 --- a/tests/data/preview/return_annotation_brackets_string.py +++ b/tests/data/preview/return_annotation_brackets_string.py @@ -2,6 +2,10 @@ def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": pass +# splitting the string breaks if there's any parameters +def frobnicate(a) -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": + pass + # output # Long string example @@ -10,3 +14,10 @@ def frobnicate() -> ( " list[ThisIsTrulyUnreasonablyExtremelyLongClassName]" ): pass + + +# splitting the string breaks if there's any parameters +def frobnicate( + a, +) -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": + pass diff --git a/tests/data/preview_py_310/funcdef_return_type_trailing_comma.py b/tests/data/preview_py_310/funcdef_return_type_trailing_comma.py new file mode 100644 index 00000000000..15db772f01e --- /dev/null +++ b/tests/data/preview_py_310/funcdef_return_type_trailing_comma.py @@ -0,0 +1,300 @@ +# normal, short, function definition +def foo(a, b) -> tuple[int, float]: ... + + +# normal, short, function definition w/o return type +def foo(a, b): ... + + +# no splitting +def foo(a: A, b: B) -> list[p, q]: + pass + + +# magic trailing comma in param list +def foo(a, b,): ... + + +# magic trailing comma in nested params in param list +def foo(a, b: tuple[int, float,]): ... + + +# magic trailing comma in return type, no params +def a() -> tuple[ + a, + b, +]: ... + + +# magic trailing comma in return type, params +def foo(a: A, b: B) -> list[ + p, + q, +]: + pass + + +# magic trailing comma in param list and in return type +def foo( + a: a, + b: b, +) -> list[ + a, + a, +]: + pass + + +# long function definition, param list is longer +def aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + bbbbbbbbbbbbbbbbbb, +) -> cccccccccccccccccccccccccccccc: ... + + +# long function definition, return type is longer +# this should maybe split on rhs? +def aaaaaaaaaaaaaaaaa(bbbbbbbbbbbbbbbbbb) -> list[ + Ccccccccccccccccccccccccccccccccccccccccccccccccccc, Dddddd +]: ... + + +# long return type, no param list +def foo() -> list[ + Loooooooooooooooooooooooooooooooooooong, + Loooooooooooooooooooong, + Looooooooooooong, +]: ... + + +# long function name, no param list, no return value +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong(): + pass + + +# long function name, no param list +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong() -> ( + list[int, float] +): ... + + +# long function name, no return value +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong( + a, b +): ... + + +# unskippable type hint (??) +def foo(a) -> list[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: # type: ignore + pass + + +def foo(a) -> list[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]: # abpedeifnore + pass + +def foo(a, b: list[Bad],): ... # type: ignore + +# don't lose any comments (no magic) +def foo( # 1 + a, # 2 + b) -> list[ # 3 + a, # 4 + b]: # 5 + ... # 6 + + +# don't lose any comments (param list magic) +def foo( # 1 + a, # 2 + b,) -> list[ # 3 + a, # 4 + b]: # 5 + ... # 6 + + +# don't lose any comments (return type magic) +def foo( # 1 + a, # 2 + b) -> list[ # 3 + a, # 4 + b,]: # 5 + ... # 6 + + +# don't lose any comments (both magic) +def foo( # 1 + a, # 2 + b,) -> list[ # 3 + a, # 4 + b,]: # 5 + ... # 6 + +# real life example +def SimplePyFn( + context: hl.GeneratorContext, + buffer_input: Buffer[UInt8, 2], + func_input: Buffer[Int32, 2], + float_arg: Scalar[Float32], + offset: int = 0, +) -> tuple[ + Buffer[UInt8, 2], + Buffer[UInt8, 2], +]: ... +# output +# normal, short, function definition +def foo(a, b) -> tuple[int, float]: ... + + +# normal, short, function definition w/o return type +def foo(a, b): ... + + +# no splitting +def foo(a: A, b: B) -> list[p, q]: + pass + + +# magic trailing comma in param list +def foo( + a, + b, +): ... + + +# magic trailing comma in nested params in param list +def foo( + a, + b: tuple[ + int, + float, + ], +): ... + + +# magic trailing comma in return type, no params +def a() -> tuple[ + a, + b, +]: ... + + +# magic trailing comma in return type, params +def foo(a: A, b: B) -> list[ + p, + q, +]: + pass + + +# magic trailing comma in param list and in return type +def foo( + a: a, + b: b, +) -> list[ + a, + a, +]: + pass + + +# long function definition, param list is longer +def aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + bbbbbbbbbbbbbbbbbb, +) -> cccccccccccccccccccccccccccccc: ... + + +# long function definition, return type is longer +# this should maybe split on rhs? +def aaaaaaaaaaaaaaaaa( + bbbbbbbbbbbbbbbbbb, +) -> list[Ccccccccccccccccccccccccccccccccccccccccccccccccccc, Dddddd]: ... + + +# long return type, no param list +def foo() -> list[ + Loooooooooooooooooooooooooooooooooooong, + Loooooooooooooooooooong, + Looooooooooooong, +]: ... + + +# long function name, no param list, no return value +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong(): + pass + + +# long function name, no param list +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong() -> ( + list[int, float] +): ... + + +# long function name, no return value +def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong( + a, b +): ... + + +# unskippable type hint (??) +def foo(a) -> list[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: # type: ignore + pass + + +def foo( + a, +) -> list[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +]: # abpedeifnore + pass + + +def foo( + a, + b: list[Bad], +): ... # type: ignore + + +# don't lose any comments (no magic) +def foo(a, b) -> list[a, b]: # 1 # 2 # 3 # 4 # 5 + ... # 6 + + +# don't lose any comments (param list magic) +def foo( # 1 + a, # 2 + b, +) -> list[a, b]: # 3 # 4 # 5 + ... # 6 + + +# don't lose any comments (return type magic) +def foo(a, b) -> list[ # 1 # 2 # 3 + a, # 4 + b, +]: # 5 + ... # 6 + + +# don't lose any comments (both magic) +def foo( # 1 + a, # 2 + b, +) -> list[ # 3 + a, # 4 + b, +]: # 5 + ... # 6 + + +# real life example +def SimplePyFn( + context: hl.GeneratorContext, + buffer_input: Buffer[UInt8, 2], + func_input: Buffer[Int32, 2], + float_arg: Scalar[Float32], + offset: int = 0, +) -> tuple[ + Buffer[UInt8, 2], + Buffer[UInt8, 2], +]: ... diff --git a/tests/data/simple_cases/return_annotation_brackets.py b/tests/data/simple_cases/return_annotation_brackets.py index 265c30220d8..8509ecdb92c 100644 --- a/tests/data/simple_cases/return_annotation_brackets.py +++ b/tests/data/simple_cases/return_annotation_brackets.py @@ -87,6 +87,11 @@ def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo def foo() -> tuple[int, int, int,]: return 2 +# Magic trailing comma example, with params +# this is broken - the trailing comma is transferred to the param list. Fixed in preview +def foo(a,b) -> tuple[int, int, int,]: + return 2 + # output # Control def double(a: int) -> int: @@ -208,3 +213,11 @@ def foo() -> ( ] ): return 2 + + +# Magic trailing comma example, with params +# this is broken - the trailing comma is transferred to the param list. Fixed in preview +def foo( + a, b +) -> tuple[int, int, int,]: + return 2 From 6c88e8e46e19aa9f1986ab9d1f0ee4cf95f49956 Mon Sep 17 00:00:00 2001 From: Jake Anto <64896514+jake-anto@users.noreply.github.com> Date: Fri, 6 Oct 2023 06:44:59 +0530 Subject: [PATCH 572/700] Update link to VS Code formatting instructions (#3921) Update link --- docs/integrations/editors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 83904144c48..7d056160fcb 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -391,7 +391,7 @@ close and reopen your File, _Black_ will be done with its job. - Use the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) - ([instructions](https://code.visualstudio.com/docs/python/editing#_formatting)). + ([instructions](https://code.visualstudio.com/docs/python/formatting)). - Alternatively the pre-release [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) From 27c05e1e24e02c62d0c2de2a1ab0673b2c2ca653 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Fri, 6 Oct 2023 03:15:35 +0200 Subject: [PATCH 573/700] exclude tests/data/.* from mypy (#3917) --- mypy.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy.ini b/mypy.ini index 95ec22d65be..ad916185bce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -39,3 +39,8 @@ ignore_missing_imports = True [mypy-_black_version.*] ignore_missing_imports = True + +# CI only checks src/, but in case users are running LSP or similar we explicitly ignore +# errors in test data files. +[mypy-tests.data.*] +ignore_errors = True From 3a2d76c7bcf39e852f3b379b76537d7847ed4225 Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Fri, 6 Oct 2023 04:21:56 +0300 Subject: [PATCH 574/700] =?UTF-8?q?Remove=20`$`,=20`>>>`=20and=20other=20p?= =?UTF-8?q?rompt=20prefixes=20when=20code=20copied=20from=20the=E2=80=A6?= =?UTF-8?q?=20(#3884)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding configurations for sphinx-copybutton in conf.py https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index f7cf1b42842..6b645435325 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -210,6 +210,13 @@ def make_pypi_svg(version: str) -> None: autodoc_member_order = "bysource" +# -- sphinx-copybutton configuration ---------------------------------------- +copybutton_prompt_text = ( + r">>> |\.\.\. |> |\$ |\# | In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +) +copybutton_prompt_is_regexp = True +copybutton_remove_prompts = True + # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. From 3457ec48af0f4c481cdf35f280998bde3f484efd Mon Sep 17 00:00:00 2001 From: Cristiano Salerno <119511125+csalerno-asml@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:41:36 +0200 Subject: [PATCH 575/700] Update output display to job summary (#3914) * Update output display to job summary * fix: handled exit-code of script * added changelog message --- CHANGES.md | 2 ++ action.yml | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 888824ee055..062a195717d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,8 @@ +- The action output displayed in the job summary is now wrapped in Markdown (#3914) + ### Documentation +- Black no longer attempts to provide special errors for attempting to format Python 2 + code (#3933) + ### _Blackd_ diff --git a/src/black/parsing.py b/src/black/parsing.py index e98e019cac6..03e767a333b 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -3,7 +3,7 @@ """ import ast import sys -from typing import Final, Iterable, Iterator, List, Set, Tuple +from typing import Iterable, Iterator, List, Set, Tuple from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature from black.nodes import syms @@ -14,8 +14,6 @@ from blib2to3.pgen2.tokenize import TokenError from blib2to3.pytree import Leaf, Node -PY2_HINT: Final = "Python 2 support was removed in version 22.0." - class InvalidInput(ValueError): """Raised when input source code fails all parse attempts.""" @@ -26,9 +24,9 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: # No target_version specified, so try all grammars. return [ # Python 3.7-3.9 - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + pygram.python_grammar_async_keywords, # Python 3.0-3.6 - pygram.python_grammar_no_print_statement_no_exec_statement, + pygram.python_grammar, # Python 3.10+ pygram.python_grammar_soft_keywords, ] @@ -39,12 +37,10 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: target_versions, Feature.ASYNC_IDENTIFIERS ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): # Python 3.7-3.9 - grammars.append( - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords - ) + grammars.append(pygram.python_grammar_async_keywords) 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) + grammars.append(pygram.python_grammar) if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions): # Python 3.10+ grammars.append(pygram.python_grammar_soft_keywords) @@ -89,14 +85,6 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) - # 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 - ): - original_msg = exc.args[0] - msg = f"{original_msg}\n{PY2_HINT}" - raise InvalidInput(msg) from None - raise exc from None if isinstance(result, Leaf): diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index e48e66363fb..be91df75740 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -80,8 +80,8 @@ vfplist: vfpdef (',' vfpdef)* [','] stmt: simple_stmt | compound_stmt simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE -small_stmt: (type_stmt | expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | - import_stmt | global_stmt | exec_stmt | assert_stmt) +small_stmt: (type_stmt | expr_stmt | del_stmt | pass_stmt | flow_stmt | + import_stmt | global_stmt | assert_stmt) expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) | ('=' (yield_expr|testlist_star_expr))*) annassign: ':' test ['=' (yield_expr|testlist_star_expr)] @@ -89,8 +89,6 @@ testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' | '**=' | '//=') # For normal and annotated assignments, additional restrictions enforced by the interpreter -print_stmt: 'print' ( [ test (',' test)* [','] ] | - '>>' test [ (',' test)+ [','] ] ) del_stmt: 'del' exprlist pass_stmt: 'pass' flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt @@ -109,7 +107,6 @@ import_as_names: import_as_name (',' import_as_name)* [','] dotted_as_names: dotted_as_name (',' dotted_as_name)* dotted_name: NAME ('.' NAME)* global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* -exec_stmt: 'exec' expr ['in' test [',' test]] assert_stmt: 'assert' test [',' test] type_stmt: "type" NAME [typeparams] '=' expr diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index c30c630e816..2b43b4c112b 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -63,7 +63,6 @@ class _python_symbols(Symbols): encoding_decl: int eval_input: int except_clause: int - exec_stmt: int expr: int expr_stmt: int exprlist: int @@ -97,7 +96,6 @@ class _python_symbols(Symbols): pattern: int patterns: int power: int - print_stmt: int raise_stmt: int return_stmt: int shift_expr: int @@ -153,22 +151,16 @@ class _pattern_symbols(Symbols): python_grammar: Grammar -python_grammar_no_print_statement: Grammar -python_grammar_no_print_statement_no_exec_statement: Grammar -python_grammar_no_print_statement_no_exec_statement_async_keywords: Grammar -python_grammar_no_exec_statement: Grammar -pattern_grammar: Grammar +python_grammar_async_keywords: Grammar python_grammar_soft_keywords: Grammar - +pattern_grammar: Grammar python_symbols: _python_symbols pattern_symbols: _pattern_symbols def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: global python_grammar - global python_grammar_no_print_statement - global python_grammar_no_print_statement_no_exec_statement - global python_grammar_no_print_statement_no_exec_statement_async_keywords + global python_grammar_async_keywords global python_grammar_soft_keywords global python_symbols global pattern_grammar @@ -180,38 +172,25 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None: os.path.dirname(__file__), "PatternGrammar.txt" ) - # Python 2 python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir) - python_grammar.version = (2, 0) + assert "print" not in python_grammar.keywords + assert "exec" not in python_grammar.keywords soft_keywords = python_grammar.soft_keywords.copy() python_grammar.soft_keywords.clear() python_symbols = _python_symbols(python_grammar) - # Python 2 + from __future__ import print_function - python_grammar_no_print_statement = python_grammar.copy() - del python_grammar_no_print_statement.keywords["print"] - # Python 3.0-3.6 - 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_grammar.version = (3, 0) # Python 3.7+ - python_grammar_no_print_statement_no_exec_statement_async_keywords = ( - python_grammar_no_print_statement_no_exec_statement.copy() - ) - 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_grammar_async_keywords = python_grammar.copy() + python_grammar_async_keywords.async_keywords = True + python_grammar_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 = python_grammar_async_keywords.copy() python_grammar_soft_keywords.soft_keywords = soft_keywords python_grammar_soft_keywords.version = (3, 10) diff --git a/tests/test_format.py b/tests/test_format.py index f3db423b637..ff358d59c94 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -155,12 +155,6 @@ def test_fast_cases(filename: str) -> None: assert_format(source, expected, fast=True) -def test_python_2_hint() -> None: - with pytest.raises(black.parsing.InvalidInput) as exc_info: - assert_format("print 'daylily'", "print 'daylily'") - exc_info.match(black.parsing.PY2_HINT) - - @pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") def test_docstring_no_string_normalization() -> None: """Like test_docstring but with string normalization off.""" From a69bda3b9bde208d5489eb2e37fc982b6bc1d8df Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 9 Oct 2023 18:43:47 -0700 Subject: [PATCH 579/700] Use inline flags for test cases (#3931) Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- docs/contributing/the_basics.md | 21 +- .../attribute_access_on_number_literals.py | 0 .../beginning_backslash.py | 0 .../{simple_cases => cases}/bracketmatch.py | 0 .../class_blank_parentheses.py | 0 .../class_methods_new_line.py | 0 .../{simple_cases => cases}/collections.py | 0 .../comment_after_escaped_newline.py | 0 .../data/{simple_cases => cases}/comments.py | 0 .../data/{simple_cases => cases}/comments2.py | 0 .../data/{simple_cases => cases}/comments3.py | 0 .../data/{simple_cases => cases}/comments4.py | 0 .../data/{simple_cases => cases}/comments5.py | 0 .../data/{simple_cases => cases}/comments6.py | 0 .../data/{simple_cases => cases}/comments8.py | 0 .../data/{simple_cases => cases}/comments9.py | 0 .../comments_non_breaking_space.py | 0 .../{simple_cases => cases}/composition.py | 0 .../composition_no_trailing_comma.py | 0 .../data/{simple_cases => cases}/docstring.py | 0 ...ocstring_no_extra_empty_line_before_eof.py | 0 .../docstring_no_string_normalization.py | 1 + .../docstring_preview.py | 0 ...cstring_preview_no_string_normalization.py | 1 + .../{simple_cases => cases}/empty_lines.py | 0 .../{simple_cases => cases}/expression.diff | 0 .../{simple_cases => cases}/expression.py | 0 .../data/{simple_cases => cases}/fmtonoff.py | 0 .../data/{simple_cases => cases}/fmtonoff2.py | 0 .../data/{simple_cases => cases}/fmtonoff3.py | 0 .../data/{simple_cases => cases}/fmtonoff4.py | 0 .../data/{simple_cases => cases}/fmtonoff5.py | 0 .../fmtpass_imports.py | 0 tests/data/{simple_cases => cases}/fmtskip.py | 0 .../data/{simple_cases => cases}/fmtskip2.py | 0 .../data/{simple_cases => cases}/fmtskip3.py | 0 .../data/{simple_cases => cases}/fmtskip4.py | 0 .../data/{simple_cases => cases}/fmtskip5.py | 0 .../data/{simple_cases => cases}/fmtskip6.py | 0 .../data/{simple_cases => cases}/fmtskip7.py | 0 .../data/{simple_cases => cases}/fmtskip8.py | 0 tests/data/{simple_cases => cases}/fstring.py | 0 .../funcdef_return_type_trailing_comma.py | 1 + .../data/{simple_cases => cases}/function.py | 0 .../data/{simple_cases => cases}/function2.py | 0 .../function_trailing_comma.py | 0 .../{simple_cases => cases}/ignore_pyi.py | 1 + .../{simple_cases => cases}/import_spacing.py | 0 .../{miscellaneous => cases}/linelength6.py | 1 + .../long_strings_flag_disabled.py | 0 ...ine_consecutive_open_parentheses_ignore.py | 0 .../nested_stub.pyi => cases/nested_stub.py} | 1 + .../data/{py_36 => cases}/numeric_literals.py | 5 - .../numeric_literals_skip_underscores.py | 4 - .../one_element_subscript.py | 0 .../parenthesized_context_managers.py | 1 + .../pattern_matching_complex.py | 1 + .../pattern_matching_extras.py | 1 + .../pattern_matching_generic.py | 1 + .../pattern_matching_simple.py | 1 + .../pattern_matching_style.py | 1 + .../pep604_union_types_line_breaks.py | 1 + tests/data/{py_38 => cases}/pep_570.py | 1 + tests/data/{py_38 => cases}/pep_572.py | 1 + .../pep_572_do_not_remove_parens.py | 1 + tests/data/{py_310 => cases}/pep_572_py310.py | 1 + tests/data/{py_39 => cases}/pep_572_py39.py | 1 + .../{py_38 => cases}/pep_572_remove_parens.py | 1 + tests/data/{simple_cases => cases}/pep_604.py | 0 tests/data/{py_311 => cases}/pep_646.py | 1 + tests/data/{py_311 => cases}/pep_654.py | 1 + tests/data/{py_311 => cases}/pep_654_style.py | 1 + .../power_op_newline.py | 1 + .../power_op_spacing.py | 0 .../prefer_rhs_split_reformatted.py | 0 .../preview_async_stmts.py} | 1 + .../cantfit.py => cases/preview_cantfit.py} | 1 + .../preview_comments7.py} | 1 + .../preview_context_managers_38.py} | 1 + .../preview_context_managers_39.py} | 1 + ...review_context_managers_autodetect_310.py} | 1 + ...review_context_managers_autodetect_311.py} | 1 + ...preview_context_managers_autodetect_38.py} | 1 + ...preview_context_managers_autodetect_39.py} | 1 + .../preview_dummy_implementations.py} | 1 + .../preview_format_unicode_escape_seq.py} | 1 + .../preview_long_dict_values.py} | 1 + .../preview_long_strings.py} | 1 + ...preview_long_strings__east_asian_width.py} | 1 + .../preview_long_strings__edge_case.py} | 1 + .../preview_long_strings__regression.py} | 1 + ...preview_long_strings__type_annotations.py} | 1 + .../preview_multiline_strings.py} | 1 + ...preview_no_blank_line_before_docstring.py} | 1 + .../pep_572.py => cases/preview_pep_572.py} | 1 + .../preview_percent_precedence.py} | 1 + .../preview_prefer_rhs_split.py} | 1 + ...view_return_annotation_brackets_string.py} | 1 + .../preview_trailing_comma.py} | 1 + .../pep_572.py => cases/py310_pep572.py} | 1 + tests/data/{py_37 => cases}/python37.py | 5 +- tests/data/{py_38 => cases}/python38.py | 5 +- tests/data/{py_39 => cases}/python39.py | 6 +- .../remove_await_parens.py | 0 .../remove_except_parens.py | 0 .../remove_for_brackets.py | 0 .../remove_newline_after_code_block_open.py | 0 .../remove_newline_after_match.py | 1 + .../{simple_cases => cases}/remove_parens.py | 0 .../{py_39 => cases}/remove_with_brackets.py | 1 + .../return_annotation_brackets.py | 0 .../skip_magic_trailing_comma.py | 1 + tests/data/{simple_cases => cases}/slices.py | 0 .../{py_310 => cases}/starred_for_target.py | 1 + .../string_prefixes.py | 0 .../{miscellaneous/stub.pyi => cases/stub.py} | 1 + tests/data/{simple_cases => cases}/torture.py | 0 .../trailing_comma_optional_parens1.py | 0 .../trailing_comma_optional_parens2.py | 0 .../trailing_comma_optional_parens3.py | 0 .../trailing_commas_in_leading_parts.py | 0 .../tricky_unicode_symbols.py | 0 .../{simple_cases => cases}/tupleassign.py | 0 tests/data/{py_312 => cases}/type_aliases.py | 1 + .../type_comment_syntax_error.py | 0 tests/data/{py_312 => cases}/type_params.py | 1 + .../{simple_cases => cases}/whitespace.py | 0 tests/data/miscellaneous/force_pyi.py | 1 + tests/test_black.py | 42 ++-- tests/test_blackd.py | 2 +- tests/test_format.py | 194 ++---------------- tests/util.py | 86 +++++++- 132 files changed, 206 insertions(+), 220 deletions(-) rename tests/data/{simple_cases => cases}/attribute_access_on_number_literals.py (100%) rename tests/data/{simple_cases => cases}/beginning_backslash.py (100%) rename tests/data/{simple_cases => cases}/bracketmatch.py (100%) rename tests/data/{simple_cases => cases}/class_blank_parentheses.py (100%) rename tests/data/{simple_cases => cases}/class_methods_new_line.py (100%) rename tests/data/{simple_cases => cases}/collections.py (100%) rename tests/data/{simple_cases => cases}/comment_after_escaped_newline.py (100%) rename tests/data/{simple_cases => cases}/comments.py (100%) rename tests/data/{simple_cases => cases}/comments2.py (100%) rename tests/data/{simple_cases => cases}/comments3.py (100%) rename tests/data/{simple_cases => cases}/comments4.py (100%) rename tests/data/{simple_cases => cases}/comments5.py (100%) rename tests/data/{simple_cases => cases}/comments6.py (100%) rename tests/data/{simple_cases => cases}/comments8.py (100%) rename tests/data/{simple_cases => cases}/comments9.py (100%) rename tests/data/{simple_cases => cases}/comments_non_breaking_space.py (100%) rename tests/data/{simple_cases => cases}/composition.py (100%) rename tests/data/{simple_cases => cases}/composition_no_trailing_comma.py (100%) rename tests/data/{simple_cases => cases}/docstring.py (100%) rename tests/data/{simple_cases => cases}/docstring_no_extra_empty_line_before_eof.py (100%) rename tests/data/{miscellaneous => cases}/docstring_no_string_normalization.py (98%) rename tests/data/{simple_cases => cases}/docstring_preview.py (100%) rename tests/data/{miscellaneous => cases}/docstring_preview_no_string_normalization.py (88%) rename tests/data/{simple_cases => cases}/empty_lines.py (100%) rename tests/data/{simple_cases => cases}/expression.diff (100%) rename tests/data/{simple_cases => cases}/expression.py (100%) rename tests/data/{simple_cases => cases}/fmtonoff.py (100%) rename tests/data/{simple_cases => cases}/fmtonoff2.py (100%) rename tests/data/{simple_cases => cases}/fmtonoff3.py (100%) rename tests/data/{simple_cases => cases}/fmtonoff4.py (100%) rename tests/data/{simple_cases => cases}/fmtonoff5.py (100%) rename tests/data/{simple_cases => cases}/fmtpass_imports.py (100%) rename tests/data/{simple_cases => cases}/fmtskip.py (100%) rename tests/data/{simple_cases => cases}/fmtskip2.py (100%) rename tests/data/{simple_cases => cases}/fmtskip3.py (100%) rename tests/data/{simple_cases => cases}/fmtskip4.py (100%) rename tests/data/{simple_cases => cases}/fmtskip5.py (100%) rename tests/data/{simple_cases => cases}/fmtskip6.py (100%) rename tests/data/{simple_cases => cases}/fmtskip7.py (100%) rename tests/data/{simple_cases => cases}/fmtskip8.py (100%) rename tests/data/{simple_cases => cases}/fstring.py (100%) rename tests/data/{preview_py_310 => cases}/funcdef_return_type_trailing_comma.py (99%) rename tests/data/{simple_cases => cases}/function.py (100%) rename tests/data/{simple_cases => cases}/function2.py (100%) rename tests/data/{simple_cases => cases}/function_trailing_comma.py (100%) rename tests/data/{simple_cases => cases}/ignore_pyi.py (97%) rename tests/data/{simple_cases => cases}/import_spacing.py (100%) rename tests/data/{miscellaneous => cases}/linelength6.py (80%) rename tests/data/{miscellaneous => cases}/long_strings_flag_disabled.py (100%) rename tests/data/{simple_cases => cases}/multiline_consecutive_open_parentheses_ignore.py (100%) rename tests/data/{miscellaneous/nested_stub.pyi => cases/nested_stub.py} (97%) rename tests/data/{py_36 => cases}/numeric_literals.py (91%) rename tests/data/{py_36 => cases}/numeric_literals_skip_underscores.py (80%) rename tests/data/{simple_cases => cases}/one_element_subscript.py (100%) rename tests/data/{py_310 => cases}/parenthesized_context_managers.py (95%) rename tests/data/{py_310 => cases}/pattern_matching_complex.py (98%) rename tests/data/{py_310 => cases}/pattern_matching_extras.py (98%) rename tests/data/{py_310 => cases}/pattern_matching_generic.py (98%) rename tests/data/{py_310 => cases}/pattern_matching_simple.py (98%) rename tests/data/{py_310 => cases}/pattern_matching_style.py (97%) rename tests/data/{preview_py_310 => cases}/pep604_union_types_line_breaks.py (99%) rename tests/data/{py_38 => cases}/pep_570.py (95%) rename tests/data/{py_38 => cases}/pep_572.py (96%) rename tests/data/{fast => cases}/pep_572_do_not_remove_parens.py (96%) rename tests/data/{py_310 => cases}/pep_572_py310.py (93%) rename tests/data/{py_39 => cases}/pep_572_py39.py (89%) rename tests/data/{py_38 => cases}/pep_572_remove_parens.py (98%) rename tests/data/{simple_cases => cases}/pep_604.py (100%) rename tests/data/{py_311 => cases}/pep_646.py (98%) rename tests/data/{py_311 => cases}/pep_654.py (96%) rename tests/data/{py_311 => cases}/pep_654_style.py (98%) rename tests/data/{miscellaneous => cases}/power_op_newline.py (73%) rename tests/data/{simple_cases => cases}/power_op_spacing.py (100%) rename tests/data/{simple_cases => cases}/prefer_rhs_split_reformatted.py (100%) rename tests/data/{preview/async_stmts.py => cases/preview_async_stmts.py} (93%) rename tests/data/{preview/cantfit.py => cases/preview_cantfit.py} (99%) rename tests/data/{preview/comments7.py => cases/preview_comments7.py} (99%) rename tests/data/{preview_context_managers/targeting_py38.py => cases/preview_context_managers_38.py} (96%) rename tests/data/{preview_context_managers/targeting_py39.py => cases/preview_context_managers_39.py} (98%) rename tests/data/{preview_context_managers/auto_detect/features_3_10.py => cases/preview_context_managers_autodetect_310.py} (93%) rename tests/data/{preview_context_managers/auto_detect/features_3_11.py => cases/preview_context_managers_autodetect_311.py} (92%) rename tests/data/{preview_context_managers/auto_detect/features_3_8.py => cases/preview_context_managers_autodetect_38.py} (98%) rename tests/data/{preview_context_managers/auto_detect/features_3_9.py => cases/preview_context_managers_autodetect_39.py} (93%) rename tests/data/{preview/dummy_implementations.py => cases/preview_dummy_implementations.py} (98%) rename tests/data/{preview/format_unicode_escape_seq.py => cases/preview_format_unicode_escape_seq.py} (96%) rename tests/data/{preview/long_dict_values.py => cases/preview_long_dict_values.py} (99%) rename tests/data/{preview/long_strings.py => cases/preview_long_strings.py} (99%) rename tests/data/{preview/long_strings__east_asian_width.py => cases/preview_long_strings__east_asian_width.py} (96%) rename tests/data/{preview/long_strings__edge_case.py => cases/preview_long_strings__edge_case.py} (99%) rename tests/data/{preview/long_strings__regression.py => cases/preview_long_strings__regression.py} (99%) rename tests/data/{preview/long_strings__type_annotations.py => cases/preview_long_strings__type_annotations.py} (98%) rename tests/data/{preview/multiline_strings.py => cases/preview_multiline_strings.py} (99%) rename tests/data/{preview/no_blank_line_before_docstring.py => cases/preview_no_blank_line_before_docstring.py} (97%) rename tests/data/{preview/pep_572.py => cases/preview_pep_572.py} (75%) rename tests/data/{preview/percent_precedence.py => cases/preview_percent_precedence.py} (96%) rename tests/data/{preview/prefer_rhs_split.py => cases/preview_prefer_rhs_split.py} (99%) rename tests/data/{preview/return_annotation_brackets_string.py => cases/preview_return_annotation_brackets_string.py} (97%) rename tests/data/{preview/trailing_comma.py => cases/preview_trailing_comma.py} (97%) rename tests/data/{preview_py_310/pep_572.py => cases/py310_pep572.py} (77%) rename tests/data/{py_37 => cases}/python37.py (95%) rename tests/data/{py_38 => cases}/python38.py (93%) rename tests/data/{py_39 => cases}/python39.py (92%) rename tests/data/{simple_cases => cases}/remove_await_parens.py (100%) rename tests/data/{simple_cases => cases}/remove_except_parens.py (100%) rename tests/data/{simple_cases => cases}/remove_for_brackets.py (100%) rename tests/data/{simple_cases => cases}/remove_newline_after_code_block_open.py (100%) rename tests/data/{py_310 => cases}/remove_newline_after_match.py (94%) rename tests/data/{simple_cases => cases}/remove_parens.py (100%) rename tests/data/{py_39 => cases}/remove_with_brackets.py (98%) rename tests/data/{simple_cases => cases}/return_annotation_brackets.py (100%) rename tests/data/{simple_cases => cases}/skip_magic_trailing_comma.py (97%) rename tests/data/{simple_cases => cases}/slices.py (100%) rename tests/data/{py_310 => cases}/starred_for_target.py (92%) rename tests/data/{simple_cases => cases}/string_prefixes.py (100%) rename tests/data/{miscellaneous/stub.pyi => cases/stub.py} (99%) rename tests/data/{simple_cases => cases}/torture.py (100%) rename tests/data/{simple_cases => cases}/trailing_comma_optional_parens1.py (100%) rename tests/data/{simple_cases => cases}/trailing_comma_optional_parens2.py (100%) rename tests/data/{simple_cases => cases}/trailing_comma_optional_parens3.py (100%) rename tests/data/{simple_cases => cases}/trailing_commas_in_leading_parts.py (100%) rename tests/data/{simple_cases => cases}/tricky_unicode_symbols.py (100%) rename tests/data/{simple_cases => cases}/tupleassign.py (100%) rename tests/data/{py_312 => cases}/type_aliases.py (81%) rename tests/data/{type_comments => cases}/type_comment_syntax_error.py (100%) rename tests/data/{py_312 => cases}/type_params.py (97%) rename tests/data/{simple_cases => cases}/whitespace.py (100%) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 864894b491f..bc1680eecfd 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -58,7 +58,26 @@ Further examples of invoking the tests (.venv)$ tox -e py -- --print-tree-diff=False ``` -`Black` has two pytest command-line options affecting test files in `tests/data/` that +### Testing + +All aspects of the _Black_ style should be tested. Normally, tests should be created as +files in the `tests/data/cases` directory. These files consist of up to three parts: + +- A line that starts with `# flags: ` followed by a set of command-line options. For + example, if the line is `# flags: --preview --skip-magic-trailing-comma`, the test + case will be run with preview mode on and the magic trailing comma off. The options + accepted are mostly a subset of those of _Black_ itself, except for the + `--minimum-version=` flag, which should be used when testing a grammar feature that + works only in newer versions of Python. This flag ensures that we don't try to + validate the AST on older versions and tests that we autodetect the Python version + correctly when the feature is used. For the exact flags accepted, see the function + `get_flags_parser` in `tests/util.py`. If this line is omitted, the default options + are used. +- A block of Python code used as input for the formatter. +- The line `# output`, followed by the output of _Black_ when run on the previous block. + If this is omitted, the test asserts that _Black_ will leave the input code unchanged. + +_Black_ has two pytest command-line options affecting test files in `tests/data/` that are split into an input part, and an output part, separated by a line with`# output`. These can be passed to `pytest` through `tox`, or directly into pytest if not using `tox`. diff --git a/tests/data/simple_cases/attribute_access_on_number_literals.py b/tests/data/cases/attribute_access_on_number_literals.py similarity index 100% rename from tests/data/simple_cases/attribute_access_on_number_literals.py rename to tests/data/cases/attribute_access_on_number_literals.py diff --git a/tests/data/simple_cases/beginning_backslash.py b/tests/data/cases/beginning_backslash.py similarity index 100% rename from tests/data/simple_cases/beginning_backslash.py rename to tests/data/cases/beginning_backslash.py diff --git a/tests/data/simple_cases/bracketmatch.py b/tests/data/cases/bracketmatch.py similarity index 100% rename from tests/data/simple_cases/bracketmatch.py rename to tests/data/cases/bracketmatch.py diff --git a/tests/data/simple_cases/class_blank_parentheses.py b/tests/data/cases/class_blank_parentheses.py similarity index 100% rename from tests/data/simple_cases/class_blank_parentheses.py rename to tests/data/cases/class_blank_parentheses.py diff --git a/tests/data/simple_cases/class_methods_new_line.py b/tests/data/cases/class_methods_new_line.py similarity index 100% rename from tests/data/simple_cases/class_methods_new_line.py rename to tests/data/cases/class_methods_new_line.py diff --git a/tests/data/simple_cases/collections.py b/tests/data/cases/collections.py similarity index 100% rename from tests/data/simple_cases/collections.py rename to tests/data/cases/collections.py diff --git a/tests/data/simple_cases/comment_after_escaped_newline.py b/tests/data/cases/comment_after_escaped_newline.py similarity index 100% rename from tests/data/simple_cases/comment_after_escaped_newline.py rename to tests/data/cases/comment_after_escaped_newline.py diff --git a/tests/data/simple_cases/comments.py b/tests/data/cases/comments.py similarity index 100% rename from tests/data/simple_cases/comments.py rename to tests/data/cases/comments.py diff --git a/tests/data/simple_cases/comments2.py b/tests/data/cases/comments2.py similarity index 100% rename from tests/data/simple_cases/comments2.py rename to tests/data/cases/comments2.py diff --git a/tests/data/simple_cases/comments3.py b/tests/data/cases/comments3.py similarity index 100% rename from tests/data/simple_cases/comments3.py rename to tests/data/cases/comments3.py diff --git a/tests/data/simple_cases/comments4.py b/tests/data/cases/comments4.py similarity index 100% rename from tests/data/simple_cases/comments4.py rename to tests/data/cases/comments4.py diff --git a/tests/data/simple_cases/comments5.py b/tests/data/cases/comments5.py similarity index 100% rename from tests/data/simple_cases/comments5.py rename to tests/data/cases/comments5.py diff --git a/tests/data/simple_cases/comments6.py b/tests/data/cases/comments6.py similarity index 100% rename from tests/data/simple_cases/comments6.py rename to tests/data/cases/comments6.py diff --git a/tests/data/simple_cases/comments8.py b/tests/data/cases/comments8.py similarity index 100% rename from tests/data/simple_cases/comments8.py rename to tests/data/cases/comments8.py diff --git a/tests/data/simple_cases/comments9.py b/tests/data/cases/comments9.py similarity index 100% rename from tests/data/simple_cases/comments9.py rename to tests/data/cases/comments9.py diff --git a/tests/data/simple_cases/comments_non_breaking_space.py b/tests/data/cases/comments_non_breaking_space.py similarity index 100% rename from tests/data/simple_cases/comments_non_breaking_space.py rename to tests/data/cases/comments_non_breaking_space.py diff --git a/tests/data/simple_cases/composition.py b/tests/data/cases/composition.py similarity index 100% rename from tests/data/simple_cases/composition.py rename to tests/data/cases/composition.py diff --git a/tests/data/simple_cases/composition_no_trailing_comma.py b/tests/data/cases/composition_no_trailing_comma.py similarity index 100% rename from tests/data/simple_cases/composition_no_trailing_comma.py rename to tests/data/cases/composition_no_trailing_comma.py diff --git a/tests/data/simple_cases/docstring.py b/tests/data/cases/docstring.py similarity index 100% rename from tests/data/simple_cases/docstring.py rename to tests/data/cases/docstring.py diff --git a/tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py b/tests/data/cases/docstring_no_extra_empty_line_before_eof.py similarity index 100% rename from tests/data/simple_cases/docstring_no_extra_empty_line_before_eof.py rename to tests/data/cases/docstring_no_extra_empty_line_before_eof.py diff --git a/tests/data/miscellaneous/docstring_no_string_normalization.py b/tests/data/cases/docstring_no_string_normalization.py similarity index 98% rename from tests/data/miscellaneous/docstring_no_string_normalization.py rename to tests/data/cases/docstring_no_string_normalization.py index a90b578f09a..4ec6b8a0153 100644 --- a/tests/data/miscellaneous/docstring_no_string_normalization.py +++ b/tests/data/cases/docstring_no_string_normalization.py @@ -1,3 +1,4 @@ +# flags: --skip-string-normalization class ALonelyClass: ''' A multiline class docstring. diff --git a/tests/data/simple_cases/docstring_preview.py b/tests/data/cases/docstring_preview.py similarity index 100% rename from tests/data/simple_cases/docstring_preview.py rename to tests/data/cases/docstring_preview.py diff --git a/tests/data/miscellaneous/docstring_preview_no_string_normalization.py b/tests/data/cases/docstring_preview_no_string_normalization.py similarity index 88% rename from tests/data/miscellaneous/docstring_preview_no_string_normalization.py rename to tests/data/cases/docstring_preview_no_string_normalization.py index 338cc01d33e..712c7364f51 100644 --- a/tests/data/miscellaneous/docstring_preview_no_string_normalization.py +++ b/tests/data/cases/docstring_preview_no_string_normalization.py @@ -1,3 +1,4 @@ +# flags: --preview --skip-string-normalization def do_not_touch_this_prefix(): R"""There was a bug where docstring prefixes would be normalized even with -S.""" diff --git a/tests/data/simple_cases/empty_lines.py b/tests/data/cases/empty_lines.py similarity index 100% rename from tests/data/simple_cases/empty_lines.py rename to tests/data/cases/empty_lines.py diff --git a/tests/data/simple_cases/expression.diff b/tests/data/cases/expression.diff similarity index 100% rename from tests/data/simple_cases/expression.diff rename to tests/data/cases/expression.diff diff --git a/tests/data/simple_cases/expression.py b/tests/data/cases/expression.py similarity index 100% rename from tests/data/simple_cases/expression.py rename to tests/data/cases/expression.py diff --git a/tests/data/simple_cases/fmtonoff.py b/tests/data/cases/fmtonoff.py similarity index 100% rename from tests/data/simple_cases/fmtonoff.py rename to tests/data/cases/fmtonoff.py diff --git a/tests/data/simple_cases/fmtonoff2.py b/tests/data/cases/fmtonoff2.py similarity index 100% rename from tests/data/simple_cases/fmtonoff2.py rename to tests/data/cases/fmtonoff2.py diff --git a/tests/data/simple_cases/fmtonoff3.py b/tests/data/cases/fmtonoff3.py similarity index 100% rename from tests/data/simple_cases/fmtonoff3.py rename to tests/data/cases/fmtonoff3.py diff --git a/tests/data/simple_cases/fmtonoff4.py b/tests/data/cases/fmtonoff4.py similarity index 100% rename from tests/data/simple_cases/fmtonoff4.py rename to tests/data/cases/fmtonoff4.py diff --git a/tests/data/simple_cases/fmtonoff5.py b/tests/data/cases/fmtonoff5.py similarity index 100% rename from tests/data/simple_cases/fmtonoff5.py rename to tests/data/cases/fmtonoff5.py diff --git a/tests/data/simple_cases/fmtpass_imports.py b/tests/data/cases/fmtpass_imports.py similarity index 100% rename from tests/data/simple_cases/fmtpass_imports.py rename to tests/data/cases/fmtpass_imports.py diff --git a/tests/data/simple_cases/fmtskip.py b/tests/data/cases/fmtskip.py similarity index 100% rename from tests/data/simple_cases/fmtskip.py rename to tests/data/cases/fmtskip.py diff --git a/tests/data/simple_cases/fmtskip2.py b/tests/data/cases/fmtskip2.py similarity index 100% rename from tests/data/simple_cases/fmtskip2.py rename to tests/data/cases/fmtskip2.py diff --git a/tests/data/simple_cases/fmtskip3.py b/tests/data/cases/fmtskip3.py similarity index 100% rename from tests/data/simple_cases/fmtskip3.py rename to tests/data/cases/fmtskip3.py diff --git a/tests/data/simple_cases/fmtskip4.py b/tests/data/cases/fmtskip4.py similarity index 100% rename from tests/data/simple_cases/fmtskip4.py rename to tests/data/cases/fmtskip4.py diff --git a/tests/data/simple_cases/fmtskip5.py b/tests/data/cases/fmtskip5.py similarity index 100% rename from tests/data/simple_cases/fmtskip5.py rename to tests/data/cases/fmtskip5.py diff --git a/tests/data/simple_cases/fmtskip6.py b/tests/data/cases/fmtskip6.py similarity index 100% rename from tests/data/simple_cases/fmtskip6.py rename to tests/data/cases/fmtskip6.py diff --git a/tests/data/simple_cases/fmtskip7.py b/tests/data/cases/fmtskip7.py similarity index 100% rename from tests/data/simple_cases/fmtskip7.py rename to tests/data/cases/fmtskip7.py diff --git a/tests/data/simple_cases/fmtskip8.py b/tests/data/cases/fmtskip8.py similarity index 100% rename from tests/data/simple_cases/fmtskip8.py rename to tests/data/cases/fmtskip8.py diff --git a/tests/data/simple_cases/fstring.py b/tests/data/cases/fstring.py similarity index 100% rename from tests/data/simple_cases/fstring.py rename to tests/data/cases/fstring.py diff --git a/tests/data/preview_py_310/funcdef_return_type_trailing_comma.py b/tests/data/cases/funcdef_return_type_trailing_comma.py similarity index 99% rename from tests/data/preview_py_310/funcdef_return_type_trailing_comma.py rename to tests/data/cases/funcdef_return_type_trailing_comma.py index 15db772f01e..9b9b9c673de 100644 --- a/tests/data/preview_py_310/funcdef_return_type_trailing_comma.py +++ b/tests/data/cases/funcdef_return_type_trailing_comma.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.10 # normal, short, function definition def foo(a, b) -> tuple[int, float]: ... diff --git a/tests/data/simple_cases/function.py b/tests/data/cases/function.py similarity index 100% rename from tests/data/simple_cases/function.py rename to tests/data/cases/function.py diff --git a/tests/data/simple_cases/function2.py b/tests/data/cases/function2.py similarity index 100% rename from tests/data/simple_cases/function2.py rename to tests/data/cases/function2.py diff --git a/tests/data/simple_cases/function_trailing_comma.py b/tests/data/cases/function_trailing_comma.py similarity index 100% rename from tests/data/simple_cases/function_trailing_comma.py rename to tests/data/cases/function_trailing_comma.py diff --git a/tests/data/simple_cases/ignore_pyi.py b/tests/data/cases/ignore_pyi.py similarity index 97% rename from tests/data/simple_cases/ignore_pyi.py rename to tests/data/cases/ignore_pyi.py index 3ef61079bfe..4fae7530eb9 100644 --- a/tests/data/simple_cases/ignore_pyi.py +++ b/tests/data/cases/ignore_pyi.py @@ -1,3 +1,4 @@ +# flags: --pyi def f(): # type: ignore ... diff --git a/tests/data/simple_cases/import_spacing.py b/tests/data/cases/import_spacing.py similarity index 100% rename from tests/data/simple_cases/import_spacing.py rename to tests/data/cases/import_spacing.py diff --git a/tests/data/miscellaneous/linelength6.py b/tests/data/cases/linelength6.py similarity index 80% rename from tests/data/miscellaneous/linelength6.py rename to tests/data/cases/linelength6.py index 4fb342726f5..158038bf960 100644 --- a/tests/data/miscellaneous/linelength6.py +++ b/tests/data/cases/linelength6.py @@ -1,3 +1,4 @@ +# flags: --line-length=6 # Regression test for #3427, which reproes only with line length <= 6 def f(): """ diff --git a/tests/data/miscellaneous/long_strings_flag_disabled.py b/tests/data/cases/long_strings_flag_disabled.py similarity index 100% rename from tests/data/miscellaneous/long_strings_flag_disabled.py rename to tests/data/cases/long_strings_flag_disabled.py diff --git a/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py b/tests/data/cases/multiline_consecutive_open_parentheses_ignore.py similarity index 100% rename from tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py rename to tests/data/cases/multiline_consecutive_open_parentheses_ignore.py diff --git a/tests/data/miscellaneous/nested_stub.pyi b/tests/data/cases/nested_stub.py similarity index 97% rename from tests/data/miscellaneous/nested_stub.pyi rename to tests/data/cases/nested_stub.py index 15e69d854db..b81549ec115 100644 --- a/tests/data/miscellaneous/nested_stub.pyi +++ b/tests/data/cases/nested_stub.py @@ -1,3 +1,4 @@ +# flags: --pyi --preview import sys class Outer: diff --git a/tests/data/py_36/numeric_literals.py b/tests/data/cases/numeric_literals.py similarity index 91% rename from tests/data/py_36/numeric_literals.py rename to tests/data/cases/numeric_literals.py index 254da68d330..99669328744 100644 --- a/tests/data/py_36/numeric_literals.py +++ b/tests/data/cases/numeric_literals.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3.6 - x = 123456789 x = 123456 x = .1 @@ -21,9 +19,6 @@ # output - -#!/usr/bin/env python3.6 - x = 123456789 x = 123456 x = 0.1 diff --git a/tests/data/py_36/numeric_literals_skip_underscores.py b/tests/data/cases/numeric_literals_skip_underscores.py similarity index 80% rename from tests/data/py_36/numeric_literals_skip_underscores.py rename to tests/data/cases/numeric_literals_skip_underscores.py index e345bb90276..6d60bdbb34d 100644 --- a/tests/data/py_36/numeric_literals_skip_underscores.py +++ b/tests/data/cases/numeric_literals_skip_underscores.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3.6 - x = 123456789 x = 1_2_3_4_5_6_7 x = 1E+1 @@ -11,8 +9,6 @@ # output -#!/usr/bin/env python3.6 - x = 123456789 x = 1_2_3_4_5_6_7 x = 1e1 diff --git a/tests/data/simple_cases/one_element_subscript.py b/tests/data/cases/one_element_subscript.py similarity index 100% rename from tests/data/simple_cases/one_element_subscript.py rename to tests/data/cases/one_element_subscript.py diff --git a/tests/data/py_310/parenthesized_context_managers.py b/tests/data/cases/parenthesized_context_managers.py similarity index 95% rename from tests/data/py_310/parenthesized_context_managers.py rename to tests/data/cases/parenthesized_context_managers.py index 1ef09a1bd34..16645a18baa 100644 --- a/tests/data/py_310/parenthesized_context_managers.py +++ b/tests/data/cases/parenthesized_context_managers.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 with (CtxManager() as example): ... diff --git a/tests/data/py_310/pattern_matching_complex.py b/tests/data/cases/pattern_matching_complex.py similarity index 98% rename from tests/data/py_310/pattern_matching_complex.py rename to tests/data/cases/pattern_matching_complex.py index 97ee194fd39..b4355c7333a 100644 --- a/tests/data/py_310/pattern_matching_complex.py +++ b/tests/data/cases/pattern_matching_complex.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 # Cases sampled from Lib/test/test_patma.py # case black_test_patma_098 diff --git a/tests/data/py_310/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py similarity index 98% rename from tests/data/py_310/pattern_matching_extras.py rename to tests/data/cases/pattern_matching_extras.py index 0242d264e5b..1e1481d7bbe 100644 --- a/tests/data/py_310/pattern_matching_extras.py +++ b/tests/data/cases/pattern_matching_extras.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 import match match something: diff --git a/tests/data/py_310/pattern_matching_generic.py b/tests/data/cases/pattern_matching_generic.py similarity index 98% rename from tests/data/py_310/pattern_matching_generic.py rename to tests/data/cases/pattern_matching_generic.py index 00a0e4a677d..4b4d45f0bff 100644 --- a/tests/data/py_310/pattern_matching_generic.py +++ b/tests/data/cases/pattern_matching_generic.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 re.match() match = a with match() as match: diff --git a/tests/data/py_310/pattern_matching_simple.py b/tests/data/cases/pattern_matching_simple.py similarity index 98% rename from tests/data/py_310/pattern_matching_simple.py rename to tests/data/cases/pattern_matching_simple.py index 5ed62415a4b..6fa2000f0de 100644 --- a/tests/data/py_310/pattern_matching_simple.py +++ b/tests/data/cases/pattern_matching_simple.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 # Cases sampled from PEP 636 examples match command.split(): diff --git a/tests/data/py_310/pattern_matching_style.py b/tests/data/cases/pattern_matching_style.py similarity index 97% rename from tests/data/py_310/pattern_matching_style.py rename to tests/data/cases/pattern_matching_style.py index 8e18ce2ada6..2ee6ea2b6e9 100644 --- a/tests/data/py_310/pattern_matching_style.py +++ b/tests/data/cases/pattern_matching_style.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 match something: case b(): print(1+1) case c( diff --git a/tests/data/preview_py_310/pep604_union_types_line_breaks.py b/tests/data/cases/pep604_union_types_line_breaks.py similarity index 99% rename from tests/data/preview_py_310/pep604_union_types_line_breaks.py rename to tests/data/cases/pep604_union_types_line_breaks.py index 9c4ab870766..fee2b840494 100644 --- a/tests/data/preview_py_310/pep604_union_types_line_breaks.py +++ b/tests/data/cases/pep604_union_types_line_breaks.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.10 # This has always worked z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong diff --git a/tests/data/py_38/pep_570.py b/tests/data/cases/pep_570.py similarity index 95% rename from tests/data/py_38/pep_570.py rename to tests/data/cases/pep_570.py index ca8f7ab1d95..2641c2b970e 100644 --- a/tests/data/py_38/pep_570.py +++ b/tests/data/cases/pep_570.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.8 def positional_only_arg(a, /): pass diff --git a/tests/data/py_38/pep_572.py b/tests/data/cases/pep_572.py similarity index 96% rename from tests/data/py_38/pep_572.py rename to tests/data/cases/pep_572.py index d41805f1cb1..742b6d5b7e4 100644 --- a/tests/data/py_38/pep_572.py +++ b/tests/data/cases/pep_572.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.8 (a := 1) (a := a) if (match := pattern.search(data)) is None: diff --git a/tests/data/fast/pep_572_do_not_remove_parens.py b/tests/data/cases/pep_572_do_not_remove_parens.py similarity index 96% rename from tests/data/fast/pep_572_do_not_remove_parens.py rename to tests/data/cases/pep_572_do_not_remove_parens.py index 05619ddcc2b..08dba3ffdf9 100644 --- a/tests/data/fast/pep_572_do_not_remove_parens.py +++ b/tests/data/cases/pep_572_do_not_remove_parens.py @@ -1,3 +1,4 @@ +# flags: --fast # Most of the following examples are really dumb, some of them aren't even accepted by Python, # we're fixing them only so fuzzers (which follow the grammar which actually allows these # examples matter of fact!) don't yell at us :p diff --git a/tests/data/py_310/pep_572_py310.py b/tests/data/cases/pep_572_py310.py similarity index 93% rename from tests/data/py_310/pep_572_py310.py rename to tests/data/cases/pep_572_py310.py index cb82b2d23f8..9f999deeb89 100644 --- a/tests/data/py_310/pep_572_py310.py +++ b/tests/data/cases/pep_572_py310.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 # Unparenthesized walruses are now allowed in indices since Python 3.10. x[a:=0] x[a:=0, b:=1] diff --git a/tests/data/py_39/pep_572_py39.py b/tests/data/cases/pep_572_py39.py similarity index 89% rename from tests/data/py_39/pep_572_py39.py rename to tests/data/cases/pep_572_py39.py index b8b081b8c45..d1614624d99 100644 --- a/tests/data/py_39/pep_572_py39.py +++ b/tests/data/cases/pep_572_py39.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.9 # Unparenthesized walruses are now allowed in set literals & set comprehensions # since Python 3.9 {x := 1, 2, 3} diff --git a/tests/data/py_38/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py similarity index 98% rename from tests/data/py_38/pep_572_remove_parens.py rename to tests/data/cases/pep_572_remove_parens.py index b952b2940c5..24f1ac29168 100644 --- a/tests/data/py_38/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.8 if (foo := 0): pass diff --git a/tests/data/simple_cases/pep_604.py b/tests/data/cases/pep_604.py similarity index 100% rename from tests/data/simple_cases/pep_604.py rename to tests/data/cases/pep_604.py diff --git a/tests/data/py_311/pep_646.py b/tests/data/cases/pep_646.py similarity index 98% rename from tests/data/py_311/pep_646.py rename to tests/data/cases/pep_646.py index e843ecf39d8..92b568a379c 100644 --- a/tests/data/py_311/pep_646.py +++ b/tests/data/cases/pep_646.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.11 A[*b] A[*b] = 1 A diff --git a/tests/data/py_311/pep_654.py b/tests/data/cases/pep_654.py similarity index 96% rename from tests/data/py_311/pep_654.py rename to tests/data/cases/pep_654.py index 387c0816f4b..12e49180e41 100644 --- a/tests/data/py_311/pep_654.py +++ b/tests/data/cases/pep_654.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.11 try: raise OSError("blah") except* ExceptionGroup as e: diff --git a/tests/data/py_311/pep_654_style.py b/tests/data/cases/pep_654_style.py similarity index 98% rename from tests/data/py_311/pep_654_style.py rename to tests/data/cases/pep_654_style.py index 9fc7c0c84db..0d34650e098 100644 --- a/tests/data/py_311/pep_654_style.py +++ b/tests/data/cases/pep_654_style.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.11 try: raise OSError("blah") except * ExceptionGroup as e: diff --git a/tests/data/miscellaneous/power_op_newline.py b/tests/data/cases/power_op_newline.py similarity index 73% rename from tests/data/miscellaneous/power_op_newline.py rename to tests/data/cases/power_op_newline.py index 85d434d63f6..d9b31403c9d 100644 --- a/tests/data/miscellaneous/power_op_newline.py +++ b/tests/data/cases/power_op_newline.py @@ -1,3 +1,4 @@ +# flags: --line-length=0 importA;()<<0**0# # output diff --git a/tests/data/simple_cases/power_op_spacing.py b/tests/data/cases/power_op_spacing.py similarity index 100% rename from tests/data/simple_cases/power_op_spacing.py rename to tests/data/cases/power_op_spacing.py diff --git a/tests/data/simple_cases/prefer_rhs_split_reformatted.py b/tests/data/cases/prefer_rhs_split_reformatted.py similarity index 100% rename from tests/data/simple_cases/prefer_rhs_split_reformatted.py rename to tests/data/cases/prefer_rhs_split_reformatted.py diff --git a/tests/data/preview/async_stmts.py b/tests/data/cases/preview_async_stmts.py similarity index 93% rename from tests/data/preview/async_stmts.py rename to tests/data/cases/preview_async_stmts.py index fe9594b2164..0a7671be5a6 100644 --- a/tests/data/preview/async_stmts.py +++ b/tests/data/cases/preview_async_stmts.py @@ -1,3 +1,4 @@ +# flags: --preview async def func() -> (int): return 0 diff --git a/tests/data/preview/cantfit.py b/tests/data/cases/preview_cantfit.py similarity index 99% rename from tests/data/preview/cantfit.py rename to tests/data/cases/preview_cantfit.py index 0849374f776..d5da6654f0c 100644 --- a/tests/data/preview/cantfit.py +++ b/tests/data/cases/preview_cantfit.py @@ -1,3 +1,4 @@ +# flags: --preview # long variable name this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0 this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 1 # with a comment diff --git a/tests/data/preview/comments7.py b/tests/data/cases/preview_comments7.py similarity index 99% rename from tests/data/preview/comments7.py rename to tests/data/cases/preview_comments7.py index 0655de999ec..006d4f7266f 100644 --- a/tests/data/preview/comments7.py +++ b/tests/data/cases/preview_comments7.py @@ -1,3 +1,4 @@ +# flags: --preview from .config import ( Any, Bool, diff --git a/tests/data/preview_context_managers/targeting_py38.py b/tests/data/cases/preview_context_managers_38.py similarity index 96% rename from tests/data/preview_context_managers/targeting_py38.py rename to tests/data/cases/preview_context_managers_38.py index f125cdffb8a..719d94fdcc5 100644 --- a/tests/data/preview_context_managers/targeting_py38.py +++ b/tests/data/cases/preview_context_managers_38.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.8 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/cases/preview_context_managers_39.py similarity index 98% rename from tests/data/preview_context_managers/targeting_py39.py rename to tests/data/cases/preview_context_managers_39.py index c9fcf9c8ba2..589e00ad187 100644 --- a/tests/data/preview_context_managers/targeting_py39.py +++ b/tests/data/cases/preview_context_managers_39.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.9 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/preview_context_managers/auto_detect/features_3_10.py b/tests/data/cases/preview_context_managers_autodetect_310.py similarity index 93% rename from tests/data/preview_context_managers/auto_detect/features_3_10.py rename to tests/data/cases/preview_context_managers_autodetect_310.py index 1458df1cb41..a9e31076f03 100644 --- a/tests/data/preview_context_managers/auto_detect/features_3_10.py +++ b/tests/data/cases/preview_context_managers_autodetect_310.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.10 # This file uses pattern matching introduced in Python 3.10. diff --git a/tests/data/preview_context_managers/auto_detect/features_3_11.py b/tests/data/cases/preview_context_managers_autodetect_311.py similarity index 92% rename from tests/data/preview_context_managers/auto_detect/features_3_11.py rename to tests/data/cases/preview_context_managers_autodetect_311.py index f83c5330ab3..af1e83fe74c 100644 --- a/tests/data/preview_context_managers/auto_detect/features_3_11.py +++ b/tests/data/cases/preview_context_managers_autodetect_311.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.11 # This file uses except* clause in Python 3.11. diff --git a/tests/data/preview_context_managers/auto_detect/features_3_8.py b/tests/data/cases/preview_context_managers_autodetect_38.py similarity index 98% rename from tests/data/preview_context_managers/auto_detect/features_3_8.py rename to tests/data/cases/preview_context_managers_autodetect_38.py index 79e438b995e..25217a40604 100644 --- a/tests/data/preview_context_managers/auto_detect/features_3_8.py +++ b/tests/data/cases/preview_context_managers_autodetect_38.py @@ -1,3 +1,4 @@ +# flags: --preview # This file doesn't use any Python 3.9+ only grammars. diff --git a/tests/data/preview_context_managers/auto_detect/features_3_9.py b/tests/data/cases/preview_context_managers_autodetect_39.py similarity index 93% rename from tests/data/preview_context_managers/auto_detect/features_3_9.py rename to tests/data/cases/preview_context_managers_autodetect_39.py index 0d28f993108..3f72e48db9d 100644 --- a/tests/data/preview_context_managers/auto_detect/features_3_9.py +++ b/tests/data/cases/preview_context_managers_autodetect_39.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.9 # This file uses parenthesized context managers introduced in Python 3.9. diff --git a/tests/data/preview/dummy_implementations.py b/tests/data/cases/preview_dummy_implementations.py similarity index 98% rename from tests/data/preview/dummy_implementations.py rename to tests/data/cases/preview_dummy_implementations.py index e07c25ed129..98b69bf87b2 100644 --- a/tests/data/preview/dummy_implementations.py +++ b/tests/data/cases/preview_dummy_implementations.py @@ -1,3 +1,4 @@ +# flags: --preview from typing import NoReturn, Protocol, Union, overload diff --git a/tests/data/preview/format_unicode_escape_seq.py b/tests/data/cases/preview_format_unicode_escape_seq.py similarity index 96% rename from tests/data/preview/format_unicode_escape_seq.py rename to tests/data/cases/preview_format_unicode_escape_seq.py index 3440696c303..65c3d8d166e 100644 --- a/tests/data/preview/format_unicode_escape_seq.py +++ b/tests/data/cases/preview_format_unicode_escape_seq.py @@ -1,3 +1,4 @@ +# flags: --preview x = "\x1F" x = "\\x1B" x = "\\\x1B" diff --git a/tests/data/preview/long_dict_values.py b/tests/data/cases/preview_long_dict_values.py similarity index 99% rename from tests/data/preview/long_dict_values.py rename to tests/data/cases/preview_long_dict_values.py index 4c515180028..fbbacd13d1d 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/cases/preview_long_dict_values.py @@ -1,3 +1,4 @@ +# flags: --preview my_dict = { "something_something": r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" diff --git a/tests/data/preview/long_strings.py b/tests/data/cases/preview_long_strings.py similarity index 99% rename from tests/data/preview/long_strings.py rename to tests/data/cases/preview_long_strings.py index 059148729d5..5519f098774 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -1,3 +1,4 @@ +# flags: --preview x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." diff --git a/tests/data/preview/long_strings__east_asian_width.py b/tests/data/cases/preview_long_strings__east_asian_width.py similarity index 96% rename from tests/data/preview/long_strings__east_asian_width.py rename to tests/data/cases/preview_long_strings__east_asian_width.py index fb66a78ed8b..d190f422a60 100644 --- a/tests/data/preview/long_strings__east_asian_width.py +++ b/tests/data/cases/preview_long_strings__east_asian_width.py @@ -1,3 +1,4 @@ +# flags: --preview # The following strings do not have not-so-many chars, but are long enough # when these are rendered in a monospace font (if the renderer respects # Unicode East Asian Width properties). diff --git a/tests/data/preview/long_strings__edge_case.py b/tests/data/cases/preview_long_strings__edge_case.py similarity index 99% rename from tests/data/preview/long_strings__edge_case.py rename to tests/data/cases/preview_long_strings__edge_case.py index 2bc0b6ed328..a8e8971968c 100644 --- a/tests/data/preview/long_strings__edge_case.py +++ b/tests/data/cases/preview_long_strings__edge_case.py @@ -1,3 +1,4 @@ +# flags: --preview some_variable = "This string is long but not so long that it needs to be split just yet" some_variable = 'This string is long but not so long that it needs to be split just yet' some_variable = "This string is long, just long enough that it needs to be split, u get?" diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py similarity index 99% rename from tests/data/preview/long_strings__regression.py rename to tests/data/cases/preview_long_strings__regression.py index 5f0646e6029..40d5e745cc8 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -1,3 +1,4 @@ +# flags: --preview class A: def foo(): result = type(message)("") diff --git a/tests/data/preview/long_strings__type_annotations.py b/tests/data/cases/preview_long_strings__type_annotations.py similarity index 98% rename from tests/data/preview/long_strings__type_annotations.py rename to tests/data/cases/preview_long_strings__type_annotations.py index 45de882d02c..8beb877bdd1 100644 --- a/tests/data/preview/long_strings__type_annotations.py +++ b/tests/data/cases/preview_long_strings__type_annotations.py @@ -1,3 +1,4 @@ +# flags: --preview def func( arg1, arg2, diff --git a/tests/data/preview/multiline_strings.py b/tests/data/cases/preview_multiline_strings.py similarity index 99% rename from tests/data/preview/multiline_strings.py rename to tests/data/cases/preview_multiline_strings.py index bb517d128e2..dec4ef2e548 100644 --- a/tests/data/preview/multiline_strings.py +++ b/tests/data/cases/preview_multiline_strings.py @@ -1,3 +1,4 @@ +# flags: --preview """cow say""", call(3, "dogsay", textwrap.dedent("""dove diff --git a/tests/data/preview/no_blank_line_before_docstring.py b/tests/data/cases/preview_no_blank_line_before_docstring.py similarity index 97% rename from tests/data/preview/no_blank_line_before_docstring.py rename to tests/data/cases/preview_no_blank_line_before_docstring.py index a37362de100..303035a7efb 100644 --- a/tests/data/preview/no_blank_line_before_docstring.py +++ b/tests/data/cases/preview_no_blank_line_before_docstring.py @@ -1,3 +1,4 @@ +# flags: --preview def line_before_docstring(): """Please move me up""" diff --git a/tests/data/preview/pep_572.py b/tests/data/cases/preview_pep_572.py similarity index 75% rename from tests/data/preview/pep_572.py rename to tests/data/cases/preview_pep_572.py index a50e130ad9c..8e801ff6cdc 100644 --- a/tests/data/preview/pep_572.py +++ b/tests/data/cases/preview_pep_572.py @@ -1,3 +1,4 @@ +# flags: --preview x[(a:=0):] x[:(a:=0)] diff --git a/tests/data/preview/percent_precedence.py b/tests/data/cases/preview_percent_precedence.py similarity index 96% rename from tests/data/preview/percent_precedence.py rename to tests/data/cases/preview_percent_precedence.py index b895443fb46..aeaf450ff5e 100644 --- a/tests/data/preview/percent_precedence.py +++ b/tests/data/cases/preview_percent_precedence.py @@ -1,3 +1,4 @@ +# flags: --preview ("" % a) ** 2 ("" % a)[0] ("" % a)() diff --git a/tests/data/preview/prefer_rhs_split.py b/tests/data/cases/preview_prefer_rhs_split.py similarity index 99% rename from tests/data/preview/prefer_rhs_split.py rename to tests/data/cases/preview_prefer_rhs_split.py index a809eacc773..c732c33b53a 100644 --- a/tests/data/preview/prefer_rhs_split.py +++ b/tests/data/cases/preview_prefer_rhs_split.py @@ -1,3 +1,4 @@ +# flags: --preview first_item, second_item = ( some_looooooooong_module.some_looooooooooooooong_function_name( first_argument, second_argument, third_argument diff --git a/tests/data/preview/return_annotation_brackets_string.py b/tests/data/cases/preview_return_annotation_brackets_string.py similarity index 97% rename from tests/data/preview/return_annotation_brackets_string.py rename to tests/data/cases/preview_return_annotation_brackets_string.py index 9148bd045bc..fea0ea6839a 100644 --- a/tests/data/preview/return_annotation_brackets_string.py +++ b/tests/data/cases/preview_return_annotation_brackets_string.py @@ -1,3 +1,4 @@ +# flags: --preview # Long string example def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": pass diff --git a/tests/data/preview/trailing_comma.py b/tests/data/cases/preview_trailing_comma.py similarity index 97% rename from tests/data/preview/trailing_comma.py rename to tests/data/cases/preview_trailing_comma.py index 5b09c664606..bba7e7ad16d 100644 --- a/tests/data/preview/trailing_comma.py +++ b/tests/data/cases/preview_trailing_comma.py @@ -1,3 +1,4 @@ +# flags: --preview e = { "a": fun(msg, "ts"), "longggggggggggggggid": ..., diff --git a/tests/data/preview_py_310/pep_572.py b/tests/data/cases/py310_pep572.py similarity index 77% rename from tests/data/preview_py_310/pep_572.py rename to tests/data/cases/py310_pep572.py index 78d4e9e4506..172be3898d6 100644 --- a/tests/data/preview_py_310/pep_572.py +++ b/tests/data/cases/py310_pep572.py @@ -1,3 +1,4 @@ +# flags: --preview --minimum-version=3.10 x[a:=0] x[a := 0] x[a := 0, b := 1] diff --git a/tests/data/py_37/python37.py b/tests/data/cases/python37.py similarity index 95% rename from tests/data/py_37/python37.py rename to tests/data/cases/python37.py index dab8b404a73..3f61106c45d 100644 --- a/tests/data/py_37/python37.py +++ b/tests/data/cases/python37.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.7 +# flags: --minimum-version=3.7 def f(): @@ -33,9 +33,6 @@ def make_arange(n): # output -#!/usr/bin/env python3.7 - - def f(): return (i * 2 async for i in arange(42)) diff --git a/tests/data/py_38/python38.py b/tests/data/cases/python38.py similarity index 93% rename from tests/data/py_38/python38.py rename to tests/data/cases/python38.py index 63b0588bc27..919ea6aeed4 100644 --- a/tests/data/py_38/python38.py +++ b/tests/data/cases/python38.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.8 +# flags: --minimum-version=3.8 def starred_return(): @@ -22,9 +22,6 @@ def t(): # output -#!/usr/bin/env python3.8 - - def starred_return(): my_list = ["value2", "value3"] return "value1", *my_list diff --git a/tests/data/py_39/python39.py b/tests/data/cases/python39.py similarity index 92% rename from tests/data/py_39/python39.py rename to tests/data/cases/python39.py index ae67c2257eb..1b9536c1529 100644 --- a/tests/data/py_39/python39.py +++ b/tests/data/cases/python39.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.9 +# flags: --minimum-version=3.9 @relaxed_decorator[0] def f(): @@ -14,10 +14,6 @@ def f(): # output - -#!/usr/bin/env python3.9 - - @relaxed_decorator[0] def f(): ... diff --git a/tests/data/simple_cases/remove_await_parens.py b/tests/data/cases/remove_await_parens.py similarity index 100% rename from tests/data/simple_cases/remove_await_parens.py rename to tests/data/cases/remove_await_parens.py diff --git a/tests/data/simple_cases/remove_except_parens.py b/tests/data/cases/remove_except_parens.py similarity index 100% rename from tests/data/simple_cases/remove_except_parens.py rename to tests/data/cases/remove_except_parens.py diff --git a/tests/data/simple_cases/remove_for_brackets.py b/tests/data/cases/remove_for_brackets.py similarity index 100% rename from tests/data/simple_cases/remove_for_brackets.py rename to tests/data/cases/remove_for_brackets.py diff --git a/tests/data/simple_cases/remove_newline_after_code_block_open.py b/tests/data/cases/remove_newline_after_code_block_open.py similarity index 100% rename from tests/data/simple_cases/remove_newline_after_code_block_open.py rename to tests/data/cases/remove_newline_after_code_block_open.py diff --git a/tests/data/py_310/remove_newline_after_match.py b/tests/data/cases/remove_newline_after_match.py similarity index 94% rename from tests/data/py_310/remove_newline_after_match.py rename to tests/data/cases/remove_newline_after_match.py index f7bcfbf27a2..fe6592b664d 100644 --- a/tests/data/py_310/remove_newline_after_match.py +++ b/tests/data/cases/remove_newline_after_match.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 def http_status(status): match status: diff --git a/tests/data/simple_cases/remove_parens.py b/tests/data/cases/remove_parens.py similarity index 100% rename from tests/data/simple_cases/remove_parens.py rename to tests/data/cases/remove_parens.py diff --git a/tests/data/py_39/remove_with_brackets.py b/tests/data/cases/remove_with_brackets.py similarity index 98% rename from tests/data/py_39/remove_with_brackets.py rename to tests/data/cases/remove_with_brackets.py index ea58ab93a16..3ee64902a30 100644 --- a/tests/data/py_39/remove_with_brackets.py +++ b/tests/data/cases/remove_with_brackets.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.9 with (open("bla.txt")): pass diff --git a/tests/data/simple_cases/return_annotation_brackets.py b/tests/data/cases/return_annotation_brackets.py similarity index 100% rename from tests/data/simple_cases/return_annotation_brackets.py rename to tests/data/cases/return_annotation_brackets.py diff --git a/tests/data/simple_cases/skip_magic_trailing_comma.py b/tests/data/cases/skip_magic_trailing_comma.py similarity index 97% rename from tests/data/simple_cases/skip_magic_trailing_comma.py rename to tests/data/cases/skip_magic_trailing_comma.py index c020db79864..4dda5df40f0 100644 --- a/tests/data/simple_cases/skip_magic_trailing_comma.py +++ b/tests/data/cases/skip_magic_trailing_comma.py @@ -1,3 +1,4 @@ +# flags: --skip-magic-trailing-comma # We should not remove the trailing comma in a single-element subscript. a: tuple[int,] b = tuple[int,] diff --git a/tests/data/simple_cases/slices.py b/tests/data/cases/slices.py similarity index 100% rename from tests/data/simple_cases/slices.py rename to tests/data/cases/slices.py diff --git a/tests/data/py_310/starred_for_target.py b/tests/data/cases/starred_for_target.py similarity index 92% rename from tests/data/py_310/starred_for_target.py rename to tests/data/cases/starred_for_target.py index 8fc8e059ed3..13e517816d6 100644 --- a/tests/data/py_310/starred_for_target.py +++ b/tests/data/cases/starred_for_target.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.10 for x in *a, *b: print(x) diff --git a/tests/data/simple_cases/string_prefixes.py b/tests/data/cases/string_prefixes.py similarity index 100% rename from tests/data/simple_cases/string_prefixes.py rename to tests/data/cases/string_prefixes.py diff --git a/tests/data/miscellaneous/stub.pyi b/tests/data/cases/stub.py similarity index 99% rename from tests/data/miscellaneous/stub.pyi rename to tests/data/cases/stub.py index af2cd2c2c02..f3828d55ba2 100644 --- a/tests/data/miscellaneous/stub.pyi +++ b/tests/data/cases/stub.py @@ -1,3 +1,4 @@ +# flags: --pyi X: int def f(): ... diff --git a/tests/data/simple_cases/torture.py b/tests/data/cases/torture.py similarity index 100% rename from tests/data/simple_cases/torture.py rename to tests/data/cases/torture.py diff --git a/tests/data/simple_cases/trailing_comma_optional_parens1.py b/tests/data/cases/trailing_comma_optional_parens1.py similarity index 100% rename from tests/data/simple_cases/trailing_comma_optional_parens1.py rename to tests/data/cases/trailing_comma_optional_parens1.py diff --git a/tests/data/simple_cases/trailing_comma_optional_parens2.py b/tests/data/cases/trailing_comma_optional_parens2.py similarity index 100% rename from tests/data/simple_cases/trailing_comma_optional_parens2.py rename to tests/data/cases/trailing_comma_optional_parens2.py diff --git a/tests/data/simple_cases/trailing_comma_optional_parens3.py b/tests/data/cases/trailing_comma_optional_parens3.py similarity index 100% rename from tests/data/simple_cases/trailing_comma_optional_parens3.py rename to tests/data/cases/trailing_comma_optional_parens3.py diff --git a/tests/data/simple_cases/trailing_commas_in_leading_parts.py b/tests/data/cases/trailing_commas_in_leading_parts.py similarity index 100% rename from tests/data/simple_cases/trailing_commas_in_leading_parts.py rename to tests/data/cases/trailing_commas_in_leading_parts.py diff --git a/tests/data/simple_cases/tricky_unicode_symbols.py b/tests/data/cases/tricky_unicode_symbols.py similarity index 100% rename from tests/data/simple_cases/tricky_unicode_symbols.py rename to tests/data/cases/tricky_unicode_symbols.py diff --git a/tests/data/simple_cases/tupleassign.py b/tests/data/cases/tupleassign.py similarity index 100% rename from tests/data/simple_cases/tupleassign.py rename to tests/data/cases/tupleassign.py diff --git a/tests/data/py_312/type_aliases.py b/tests/data/cases/type_aliases.py similarity index 81% rename from tests/data/py_312/type_aliases.py rename to tests/data/cases/type_aliases.py index 84e07e50fe2..a3c1931c9fc 100644 --- a/tests/data/py_312/type_aliases.py +++ b/tests/data/cases/type_aliases.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.12 type A=int type Gen[T]=list[T] diff --git a/tests/data/type_comments/type_comment_syntax_error.py b/tests/data/cases/type_comment_syntax_error.py similarity index 100% rename from tests/data/type_comments/type_comment_syntax_error.py rename to tests/data/cases/type_comment_syntax_error.py diff --git a/tests/data/py_312/type_params.py b/tests/data/cases/type_params.py similarity index 97% rename from tests/data/py_312/type_params.py rename to tests/data/cases/type_params.py index 5f8ec43267c..720a775ef31 100644 --- a/tests/data/py_312/type_params.py +++ b/tests/data/cases/type_params.py @@ -1,3 +1,4 @@ +# flags: --minimum-version=3.12 def func [T ](): pass async def func [ T ] (): pass class C[ T ] : pass diff --git a/tests/data/simple_cases/whitespace.py b/tests/data/cases/whitespace.py similarity index 100% rename from tests/data/simple_cases/whitespace.py rename to tests/data/cases/whitespace.py diff --git a/tests/data/miscellaneous/force_pyi.py b/tests/data/miscellaneous/force_pyi.py index 07ed93c6879..40caf30a983 100644 --- a/tests/data/miscellaneous/force_pyi.py +++ b/tests/data/miscellaneous/force_pyi.py @@ -1,3 +1,4 @@ +# flags: --pyi from typing import Union @bird diff --git a/tests/test_black.py b/tests/test_black.py index c665eee3a6c..bb5cc1e08c7 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -187,7 +187,9 @@ def test_experimental_string_processing_warns(self) -> None: ) def test_piping(self) -> None: - source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py") + _, source, expected = read_data_from_file( + PROJECT_ROOT / "src/black/__init__.py" + ) result = BlackRunner().invoke( black.main, [ @@ -209,8 +211,8 @@ def test_piping_diff(self) -> None: r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d" r"\+\d\d:\d\d" ) - source, _ = read_data("simple_cases", "expression.py") - expected, _ = read_data("simple_cases", "expression.diff") + source, _ = read_data("cases", "expression.py") + expected, _ = read_data("cases", "expression.diff") args = [ "-", "--fast", @@ -227,7 +229,7 @@ def test_piping_diff(self) -> None: self.assertEqual(expected, actual) def test_piping_diff_with_color(self) -> None: - source, _ = read_data("simple_cases", "expression.py") + source, _ = read_data("cases", "expression.py") args = [ "-", "--fast", @@ -263,7 +265,7 @@ def _test_wip(self) -> None: black.assert_stable(source, actual, black.FileMode()) def test_pep_572_version_detection(self) -> None: - source, _ = read_data("py_38", "pep_572") + source, _ = read_data("cases", "pep_572") root = black.lib2to3_parse(source) features = black.get_features_used(root) self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features) @@ -272,7 +274,7 @@ def test_pep_572_version_detection(self) -> None: def test_pep_695_version_detection(self) -> None: for file in ("type_aliases", "type_params"): - source, _ = read_data("py_312", file) + source, _ = read_data("cases", file) root = black.lib2to3_parse(source) features = black.get_features_used(root) self.assertIn(black.Feature.TYPE_PARAMS, features) @@ -280,7 +282,7 @@ def test_pep_695_version_detection(self) -> None: self.assertIn(black.TargetVersion.PY312, versions) def test_expression_ff(self) -> None: - source, expected = read_data("simple_cases", "expression.py") + source, expected = read_data("cases", "expression.py") tmp_file = Path(black.dump_to_file(source)) try: self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES)) @@ -293,8 +295,8 @@ def test_expression_ff(self) -> None: black.assert_stable(source, actual, DEFAULT_MODE) def test_expression_diff(self) -> None: - source, _ = read_data("simple_cases", "expression.py") - expected, _ = read_data("simple_cases", "expression.diff") + source, _ = read_data("cases", "expression.py") + expected, _ = read_data("cases", "expression.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " @@ -319,8 +321,8 @@ def test_expression_diff(self) -> None: self.assertEqual(expected, actual, msg) def test_expression_diff_with_color(self) -> None: - source, _ = read_data("simple_cases", "expression.py") - expected, _ = read_data("simple_cases", "expression.diff") + source, _ = read_data("cases", "expression.py") + expected, _ = read_data("cases", "expression.diff") tmp_file = Path(black.dump_to_file(source)) try: result = BlackRunner().invoke( @@ -339,7 +341,7 @@ def test_expression_diff_with_color(self) -> None: self.assertIn("\033[0m", actual) def test_detect_pos_only_arguments(self) -> None: - source, _ = read_data("py_38", "pep_570") + source, _ = read_data("cases", "pep_570") root = black.lib2to3_parse(source) features = black.get_features_used(root) self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features) @@ -401,7 +403,7 @@ def test_skip_source_first_line_when_mixing_newlines(self) -> None: self.assertEqual(test_file.read_bytes(), expected) def test_skip_magic_trailing_comma(self) -> None: - source, _ = read_data("simple_cases", "expression") + source, _ = read_data("cases", "expression") expected, _ = read_data( "miscellaneous", "expression_skip_magic_trailing_comma.diff" ) @@ -433,7 +435,7 @@ def test_skip_magic_trailing_comma(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_async_as_identifier(self) -> None: source_path = get_case_path("miscellaneous", "async_as_identifier") - source, expected = read_data_from_file(source_path) + _, source, expected = read_data_from_file(source_path) actual = fs(source) self.assertFormatEqual(expected, actual) major, minor = sys.version_info[:2] @@ -447,8 +449,8 @@ def test_async_as_identifier(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_python37(self) -> None: - source_path = get_case_path("py_37", "python37") - source, expected = read_data_from_file(source_path) + source_path = get_case_path("cases", "python37") + _, source, expected = read_data_from_file(source_path) actual = fs(source) self.assertFormatEqual(expected, actual) major, minor = sys.version_info[:2] @@ -884,7 +886,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) node = black.lib2to3_parse("123456\n") self.assertEqual(black.get_features_used(node), set()) - source, expected = read_data("simple_cases", "function") + source, expected = read_data("cases", "function") node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, @@ -894,7 +896,7 @@ def test_get_features_used(self) -> None: self.assertEqual(black.get_features_used(node), expected_features) node = black.lib2to3_parse(expected) self.assertEqual(black.get_features_used(node), expected_features) - source, expected = read_data("simple_cases", "expression") + source, expected = read_data("cases", "expression") node = black.lib2to3_parse(source) self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse(expected) @@ -1109,7 +1111,7 @@ def test_check_diff_use_together(self) -> None: src1 = get_case_path("miscellaneous", "string_quotes") self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) # Files which will not be reformatted. - src2 = get_case_path("simple_cases", "composition") + src2 = get_case_path("cases", "composition") self.invokeBlack([str(src2), "--diff", "--check"]) # Multi file command. self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) @@ -1330,7 +1332,7 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None: report = MagicMock() # Even with an existing file, since we are forcing stdin, black # should output to stdout and not modify the file inplace - p = THIS_DIR / "data" / "simple_cases" / "collections.py" + p = THIS_DIR / "data" / "cases" / "collections.py" # Make sure is_file actually returns True self.assertTrue(p.is_file()) path = Path(f"__BLACK_STDIN_FILENAME__{p}") diff --git a/tests/test_blackd.py b/tests/test_blackd.py index dd2126e6bc2..c0152de73e6 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -104,7 +104,7 @@ async def check(header_value: str, expected_status: int = 400) -> None: @unittest_run_loop async def test_blackd_pyi(self) -> None: - source, expected = read_data("miscellaneous", "stub.pyi") + source, expected = read_data("cases", "stub.py") response = await self.client.post( "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} ) diff --git a/tests/test_format.py b/tests/test_format.py index ff358d59c94..4e863c6c54b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,4 +1,3 @@ -import re from dataclasses import replace from typing import Any, Iterator from unittest.mock import patch @@ -6,13 +5,13 @@ import pytest import black +from black.mode import TargetVersion from tests.util import ( - DEFAULT_MODE, - PY36_VERSIONS, all_data_cases, assert_format, dump_to_stderr, read_data, + read_data_with_mode, ) @@ -22,61 +21,33 @@ def patch_dump_to_file(request: Any) -> Iterator[None]: yield -def check_file( - subdir: str, filename: str, mode: black.Mode, *, data: bool = True -) -> None: - source, expected = read_data(subdir, filename, data=data) - assert_format(source, expected, mode, fast=False) +def check_file(subdir: str, filename: str, *, data: bool = True) -> None: + args, source, expected = read_data_with_mode(subdir, filename, data=data) + assert_format( + source, + expected, + args.mode, + fast=args.fast, + minimum_version=args.minimum_version, + ) + if args.minimum_version is not None: + major, minor = args.minimum_version + target_version = TargetVersion[f"PY{major}{minor}"] + mode = replace(args.mode, target_versions={target_version}) + assert_format( + source, expected, mode, fast=args.fast, minimum_version=args.minimum_version + ) @pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") -@pytest.mark.parametrize("filename", all_data_cases("simple_cases")) +@pytest.mark.parametrize("filename", all_data_cases("cases")) def test_simple_format(filename: str) -> None: - magic_trailing_comma = filename != "skip_magic_trailing_comma" - mode = black.Mode( - magic_trailing_comma=magic_trailing_comma, is_pyi=filename.endswith("_pyi") - ) - check_file("simple_cases", filename, mode) - - -@pytest.mark.parametrize("filename", all_data_cases("preview")) -def test_preview_format(filename: str) -> None: - check_file("preview", filename, black.Mode(preview=True)) - - -def test_preview_context_managers_targeting_py38() -> None: - source, expected = read_data("preview_context_managers", "targeting_py38.py") - mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY38}) - assert_format(source, expected, mode, minimum_version=(3, 8)) - - -def test_preview_context_managers_targeting_py39() -> None: - source, expected = read_data("preview_context_managers", "targeting_py39.py") - mode = black.Mode(preview=True, target_versions={black.TargetVersion.PY39}) - assert_format(source, expected, mode, minimum_version=(3, 9)) - - -@pytest.mark.parametrize("filename", all_data_cases("preview_py_310")) -def test_preview_python_310(filename: str) -> None: - source, expected = read_data("preview_py_310", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY310}, preview=True) - assert_format(source, expected, mode, minimum_version=(3, 10)) - - -@pytest.mark.parametrize( - "filename", all_data_cases("preview_context_managers/auto_detect") -) -def test_preview_context_managers_auto_detect(filename: str) -> None: - match = re.match(r"features_3_(\d+)", filename) - assert match is not None, "Unexpected filename format: %s" % filename - source, expected = read_data("preview_context_managers/auto_detect", filename) - mode = black.Mode(preview=True) - assert_format(source, expected, mode, minimum_version=(3, int(match.group(1)))) + check_file("cases", filename) # =============== # -# Complex cases -# ============= # +# Unusual cases +# =============== # def test_empty() -> None: @@ -84,48 +55,6 @@ def test_empty() -> None: assert_format(source, expected) -@pytest.mark.parametrize("filename", all_data_cases("py_36")) -def test_python_36(filename: str) -> None: - source, expected = read_data("py_36", filename) - mode = black.Mode(target_versions=PY36_VERSIONS) - assert_format(source, expected, mode, minimum_version=(3, 6)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_37")) -def test_python_37(filename: str) -> None: - source, expected = read_data("py_37", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY37}) - assert_format(source, expected, mode, minimum_version=(3, 7)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_38")) -def test_python_38(filename: str) -> None: - source, expected = read_data("py_38", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY38}) - assert_format(source, expected, mode, minimum_version=(3, 8)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_39")) -def test_python_39(filename: str) -> None: - source, expected = read_data("py_39", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY39}) - assert_format(source, expected, mode, minimum_version=(3, 9)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_310")) -def test_python_310(filename: str) -> None: - source, expected = read_data("py_310", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY310}) - assert_format(source, expected, mode, minimum_version=(3, 10)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_310")) -def test_python_310_without_target_version(filename: str) -> None: - source, expected = read_data("py_310", filename) - mode = black.Mode() - assert_format(source, expected, mode, minimum_version=(3, 10)) - - def test_patma_invalid() -> None: source, expected = read_data("miscellaneous", "pattern_matching_invalid") mode = black.Mode(target_versions={black.TargetVersion.PY310}) @@ -133,82 +62,3 @@ def test_patma_invalid() -> None: assert_format(source, expected, mode, minimum_version=(3, 10)) exc_info.match("Cannot parse: 10:11") - - -@pytest.mark.parametrize("filename", all_data_cases("py_311")) -def test_python_311(filename: str) -> None: - source, expected = read_data("py_311", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY311}) - assert_format(source, expected, mode, minimum_version=(3, 11)) - - -@pytest.mark.parametrize("filename", all_data_cases("py_312")) -def test_python_312(filename: str) -> None: - source, expected = read_data("py_312", filename) - mode = black.Mode(target_versions={black.TargetVersion.PY312}) - assert_format(source, expected, mode, minimum_version=(3, 12)) - - -@pytest.mark.parametrize("filename", all_data_cases("fast")) -def test_fast_cases(filename: str) -> None: - source, expected = read_data("fast", filename) - assert_format(source, expected, fast=True) - - -@pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") -def test_docstring_no_string_normalization() -> None: - """Like test_docstring but with string normalization off.""" - source, expected = read_data("miscellaneous", "docstring_no_string_normalization") - mode = replace(DEFAULT_MODE, string_normalization=False) - assert_format(source, expected, mode) - - -def test_docstring_line_length_6() -> None: - """Like test_docstring but with line length set to 6.""" - source, expected = read_data("miscellaneous", "linelength6") - mode = black.Mode(line_length=6) - assert_format(source, expected, mode) - - -def test_preview_docstring_no_string_normalization() -> None: - """ - Like test_docstring but with string normalization off *and* the preview style - enabled. - """ - source, expected = read_data( - "miscellaneous", "docstring_preview_no_string_normalization" - ) - mode = replace(DEFAULT_MODE, string_normalization=False, preview=True) - assert_format(source, expected, mode) - - -def test_long_strings_flag_disabled() -> None: - """Tests for turning off the string processing logic.""" - source, expected = read_data("miscellaneous", "long_strings_flag_disabled") - mode = replace(DEFAULT_MODE, experimental_string_processing=False) - assert_format(source, expected, mode) - - -def test_stub() -> None: - mode = replace(DEFAULT_MODE, is_pyi=True) - source, expected = read_data("miscellaneous", "stub.pyi") - assert_format(source, expected, mode) - - -def test_nested_stub() -> None: - mode = replace(DEFAULT_MODE, is_pyi=True, preview=True) - source, expected = read_data("miscellaneous", "nested_stub.pyi") - assert_format(source, expected, mode) - - -def test_power_op_newline() -> None: - # requires line_length=0 - source, expected = read_data("miscellaneous", "power_op_newline") - assert_format(source, expected, mode=black.Mode(line_length=0)) - - -def test_type_comment_syntax_error() -> None: - """Test that black is able to format python code with type comment syntax errors.""" - source, expected = read_data("type_comments", "type_comment_syntax_error") - assert_format(source, expected) - black.assert_equivalent(source, expected) diff --git a/tests/util.py b/tests/util.py index 541d21da4df..a31ae0992c2 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,13 +1,17 @@ +import argparse +import functools import os +import shlex import sys import unittest from contextlib import contextmanager -from dataclasses import replace +from dataclasses import dataclass, field, replace from functools import partial from pathlib import Path from typing import Any, Iterator, List, Optional, Tuple import black +from black.const import DEFAULT_LINE_LENGTH from black.debug import DebugVisitor from black.mode import TargetVersion from black.output import diff, err, out @@ -35,6 +39,13 @@ fs = partial(black.format_str, mode=DEFAULT_MODE) +@dataclass +class TestCaseArgs: + mode: black.Mode = field(default_factory=black.Mode) + fast: bool = False + minimum_version: Optional[Tuple[int, int]] = None + + def _assert_format_equal(expected: str, actual: str) -> None: if actual != expected and (conftest.PRINT_FULL_TREE or conftest.PRINT_TREE_DIFF): bdv: DebugVisitor[Any] @@ -178,18 +189,85 @@ def get_case_path( return case_path +def read_data_with_mode( + subdir_name: str, name: str, data: bool = True +) -> Tuple[TestCaseArgs, str, str]: + """read_data_with_mode('test_name') -> Mode(), 'input', 'output'""" + return read_data_from_file(get_case_path(subdir_name, name, data)) + + def read_data(subdir_name: str, name: str, data: bool = True) -> Tuple[str, str]: """read_data('test_name') -> 'input', 'output'""" - return read_data_from_file(get_case_path(subdir_name, name, data)) + _, input, output = read_data_with_mode(subdir_name, name, data) + return input, output + + +def _parse_minimum_version(version: str) -> Tuple[int, int]: + major, minor = version.split(".") + return int(major), int(minor) -def read_data_from_file(file_name: Path) -> Tuple[str, str]: +@functools.lru_cache() +def get_flags_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument( + "--target-version", + action="append", + type=lambda val: TargetVersion[val.upper()], + default=(), + ) + parser.add_argument("--line-length", default=DEFAULT_LINE_LENGTH, type=int) + parser.add_argument( + "--skip-string-normalization", default=False, action="store_true" + ) + parser.add_argument("--pyi", default=False, action="store_true") + parser.add_argument("--ipynb", default=False, action="store_true") + parser.add_argument( + "--skip-magic-trailing-comma", default=False, action="store_true" + ) + parser.add_argument("--preview", default=False, action="store_true") + parser.add_argument("--fast", default=False, action="store_true") + parser.add_argument( + "--minimum-version", + type=_parse_minimum_version, + default=None, + help=( + "Minimum version of Python where this test case is parseable. If this is" + " set, the test case will be run twice: once with the specified" + " --target-version, and once with --target-version set to exactly the" + " specified version. This ensures that Black's autodetection of the target" + " version works correctly." + ), + ) + return parser + + +def parse_mode(flags_line: str) -> TestCaseArgs: + parser = get_flags_parser() + args = parser.parse_args(shlex.split(flags_line)) + mode = black.Mode( + target_versions=set(args.target_version), + line_length=args.line_length, + string_normalization=not args.skip_string_normalization, + is_pyi=args.pyi, + is_ipynb=args.ipynb, + magic_trailing_comma=not args.skip_magic_trailing_comma, + preview=args.preview, + ) + return TestCaseArgs(mode=mode, fast=args.fast, minimum_version=args.minimum_version) + + +def read_data_from_file(file_name: Path) -> Tuple[TestCaseArgs, str, str]: with open(file_name, "r", encoding="utf8") as test: lines = test.readlines() _input: List[str] = [] _output: List[str] = [] result = _input + mode = TestCaseArgs() for line in lines: + if not _input and line.startswith("# flags: "): + mode = parse_mode(line[len("# flags: ") :]) + continue line = line.replace(EMPTY_LINE, "") if line.rstrip() == "# output": result = _output @@ -199,7 +277,7 @@ def read_data_from_file(file_name: Path) -> Tuple[str, str]: if _input and not _output: # If there's no output marker, treat the entire file as already pre-formatted. _output = _input[:] - return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n" + return mode, "".join(_input).strip() + "\n", "".join(_output).strip() + "\n" def read_jupyter_notebook(subdir_name: str, name: str, data: bool = True) -> str: From 5d5bf6e0878539baeef797b87636235b8c02be3f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:44:36 -0700 Subject: [PATCH 580/700] Fix cache versioning when BLACK_CACHE_DIR is set (#3937) --- CHANGES.md | 2 ++ src/black/cache.py | 3 ++- tests/test_black.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ffc63b3287d..fe4b621a3e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,8 @@ +- Fix cache versioning logic when `BLACK_CACHE_DIR` is set (#3937) + ### Packaging diff --git a/src/black/cache.py b/src/black/cache.py index 77f66cc34a9..f7dc64c0bca 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -36,8 +36,9 @@ def get_cache_dir() -> Path: 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__) + default_cache_dir = user_cache_dir("black") cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir)) + cache_dir = cache_dir / __version__ return cache_dir diff --git a/tests/test_black.py b/tests/test_black.py index bb5cc1e08c7..537ca80d432 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1963,11 +1963,11 @@ def test_get_cache_dir( # 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 + assert get_cache_dir().parent == 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 + assert get_cache_dir().parent == workspace2 def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE From 7aa37ea0adf864baf3ef3dfbcfaf5ff1ff780250 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 9 Oct 2023 19:15:51 -0700 Subject: [PATCH 581/700] Report all stacktraces in verbose mode (#3938) Previously these were swallowed (unlike the ones in black/__init__.py) --- CHANGES.md | 2 ++ src/black/concurrency.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fe4b621a3e5..6ad6308945c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,8 @@ - Black no longer attempts to provide special errors for attempting to format Python 2 code (#3933) +- Black will more consistently print stacktraces on internal errors in verbose mode + (#3938) ### _Blackd_ diff --git a/src/black/concurrency.py b/src/black/concurrency.py index ce016578399..55c96b66c86 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -9,6 +9,7 @@ import os import signal import sys +import traceback from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor from multiprocessing import Manager from pathlib import Path @@ -170,8 +171,10 @@ async def schedule_formatting( src = tasks.pop(task) if task.cancelled(): cancelled.append(task) - elif task.exception(): - report.failed(src, str(task.exception())) + elif exc := task.exception(): + if report.verbose: + traceback.print_exception(type(exc), exc, exc.__traceback__) + report.failed(src, str(exc)) else: changed = Changed.YES if task.result() else Changed.NO # If the file was written back or was successfully checked as From b7717c3f1e73d6b847e2534a2cebbb657b96caf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 10 Oct 2023 04:34:26 +0200 Subject: [PATCH 582/700] Standardise newlines after module-level docstrings (#3932) Co-authored-by: jpy-git Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + scripts/make_width_table.py | 1 + src/black/cache.py | 1 + src/black/linegen.py | 1 + src/black/lines.py | 9 +++ src/black/mode.py | 1 + src/black/numerics.py | 1 + src/black/parsing.py | 1 + src/black/report.py | 1 + src/black/rusty.py | 1 + src/black/trans.py | 1 + tests/data/cases/module_docstring_1.py | 26 +++++++++ tests/data/cases/module_docstring_2.py | 68 +++++++++++++++++++++++ tests/data/cases/module_docstring_3.py | 8 +++ tests/data/cases/module_docstring_4.py | 9 +++ tests/data/miscellaneous/string_quotes.py | 2 + 16 files changed, 132 insertions(+) create mode 100644 tests/data/cases/module_docstring_1.py create mode 100644 tests/data/cases/module_docstring_2.py create mode 100644 tests/data/cases/module_docstring_3.py create mode 100644 tests/data/cases/module_docstring_4.py diff --git a/CHANGES.md b/CHANGES.md index 6ad6308945c..a608551815f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ - Long type hints are now wrapped in parentheses and properly indented when split across multiple lines (#3899) - Magic trailing commas are now respected in return types. (#3916) +- Require one empty line after module-level docstrings. (#3932) ### Configuration diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 30fd32c34b0..061fdc8d95d 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -15,6 +15,7 @@ pip install -U wcwidth """ + import sys from os.path import basename, dirname, join from typing import Iterable, Tuple diff --git a/src/black/cache.py b/src/black/cache.py index f7dc64c0bca..6baa096baca 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -1,4 +1,5 @@ """Caching of formatted files with feature-based invalidation.""" + import hashlib import os import pickle diff --git a/src/black/linegen.py b/src/black/linegen.py index bdc4ee54ab2..faeb3ba664c 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1,6 +1,7 @@ """ Generating lines of code. """ + import sys from dataclasses import replace from enum import Enum, auto diff --git a/src/black/lines.py b/src/black/lines.py index 71b657a0654..14754d7532f 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -550,6 +550,15 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: if self.previous_line is None else before - previous_after ) + if ( + Preview.module_docstring_newlines in current_line.mode + and self.previous_block + and self.previous_block.previous_block is None + and len(self.previous_block.original_line.leaves) == 1 + and self.previous_block.original_line.is_triple_quoted_string + ): + before = 1 + block = LinesBlock( mode=self.mode, previous_block=self.previous_block, diff --git a/src/black/mode.py b/src/black/mode.py index 30c5d2f1b2f..baf886abb7f 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -187,6 +187,7 @@ class Preview(Enum): wrap_multiple_context_managers_in_parens = auto() dummy_implementations = auto() walrus_subscript = auto() + module_docstring_newlines = auto() class Deprecated(UserWarning): diff --git a/src/black/numerics.py b/src/black/numerics.py index 879e5b2cf36..67ac8595fcc 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -1,6 +1,7 @@ """ Formatting numeric literals. """ + from blib2to3.pytree import Leaf diff --git a/src/black/parsing.py b/src/black/parsing.py index 03e767a333b..ea282d1805c 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -1,6 +1,7 @@ """ Parse Python code and perform AST validation. """ + import ast import sys from typing import Iterable, Iterator, List, Set, Tuple diff --git a/src/black/report.py b/src/black/report.py index a507671e4c0..89899f2f389 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -1,6 +1,7 @@ """ Summarize Black runs to users. """ + from dataclasses import dataclass from enum import Enum from pathlib import Path diff --git a/src/black/rusty.py b/src/black/rusty.py index 84a80b5a2c2..ebd4c052d1f 100644 --- a/src/black/rusty.py +++ b/src/black/rusty.py @@ -2,6 +2,7 @@ See https://doc.rust-lang.org/book/ch09-00-error-handling.html. """ + from typing import Generic, TypeVar, Union T = TypeVar("T") diff --git a/src/black/trans.py b/src/black/trans.py index c0cc92613ac..a2bff7f227a 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1,6 +1,7 @@ """ String transformers that can split and merge strings. """ + import re from abc import ABC, abstractmethod from collections import defaultdict diff --git a/tests/data/cases/module_docstring_1.py b/tests/data/cases/module_docstring_1.py new file mode 100644 index 00000000000..d5897b4db60 --- /dev/null +++ b/tests/data/cases/module_docstring_1.py @@ -0,0 +1,26 @@ +# flags: --preview +"""Single line module-level docstring should be followed by single newline.""" + + + + +a = 1 + + +"""I'm just a string so should be followed by 2 newlines.""" + + + + +b = 2 + +# output +"""Single line module-level docstring should be followed by single newline.""" + +a = 1 + + +"""I'm just a string so should be followed by 2 newlines.""" + + +b = 2 diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py new file mode 100644 index 00000000000..e1f81b4d76b --- /dev/null +++ b/tests/data/cases/module_docstring_2.py @@ -0,0 +1,68 @@ +# flags: --preview +"""I am a very helpful module docstring. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum. +""" + + + + +a = 1 + + +"""Look at me I'm a docstring... + +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +........................................................NOT! +""" + + + + +b = 2 + +# output +"""I am a very helpful module docstring. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum. +""" + +a = 1 + + +"""Look at me I'm a docstring... + +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +............................................................ +........................................................NOT! +""" + + +b = 2 diff --git a/tests/data/cases/module_docstring_3.py b/tests/data/cases/module_docstring_3.py new file mode 100644 index 00000000000..0631e136a3d --- /dev/null +++ b/tests/data/cases/module_docstring_3.py @@ -0,0 +1,8 @@ +# flags: --preview +"""Single line module-level docstring should be followed by single newline.""" +a = 1 + +# output +"""Single line module-level docstring should be followed by single newline.""" + +a = 1 diff --git a/tests/data/cases/module_docstring_4.py b/tests/data/cases/module_docstring_4.py new file mode 100644 index 00000000000..515174dcc04 --- /dev/null +++ b/tests/data/cases/module_docstring_4.py @@ -0,0 +1,9 @@ +# flags: --preview +"""Single line module-level docstring should be followed by single newline.""" + +a = 1 + +# output +"""Single line module-level docstring should be followed by single newline.""" + +a = 1 diff --git a/tests/data/miscellaneous/string_quotes.py b/tests/data/miscellaneous/string_quotes.py index 3384241f4ad..6ec088ac79b 100644 --- a/tests/data/miscellaneous/string_quotes.py +++ b/tests/data/miscellaneous/string_quotes.py @@ -1,4 +1,5 @@ '''''' + '\'' '"' "'" @@ -59,6 +60,7 @@ # output """""" + "'" '"' "'" From 935f303a0a7b794e722c7df00c906be285884874 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 9 Oct 2023 20:02:27 -0700 Subject: [PATCH 583/700] Fix test that was not being run (#3939) --- tests/data/{ => cases}/conditional_expression.py | 1 + 1 file changed, 1 insertion(+) rename tests/data/{ => cases}/conditional_expression.py (99%) diff --git a/tests/data/conditional_expression.py b/tests/data/cases/conditional_expression.py similarity index 99% rename from tests/data/conditional_expression.py rename to tests/data/cases/conditional_expression.py index 620a12dc986..c30cd76c791 100644 --- a/tests/data/conditional_expression.py +++ b/tests/data/cases/conditional_expression.py @@ -1,3 +1,4 @@ +# flags: --preview long_kwargs_single_line = my_function( foo="test, this is a sample value", bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, From 3bb92146f59804a6ead47d5f2d0fdc47daa6b698 Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Mon, 16 Oct 2023 05:13:53 -0700 Subject: [PATCH 584/700] CI Test: Deprecating 'Healthcheck.all()' from Hypothesis in fuzz.py (#3945) --- scripts/fuzz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 25362c927d4..0c507381d92 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -21,7 +21,7 @@ max_examples=1000, # roughly 1k tests/minute, or half that under coverage derandomize=True, # deterministic mode to avoid CI flakiness deadline=None, # ignore Hypothesis' health checks; we already know that - suppress_health_check=HealthCheck.all(), # this is slow and filter-heavy. + suppress_health_check=list(HealthCheck), # this is slow and filter-heavy. ) @given( # Note that while Hypothesmith might generate code unlike that written by From 6f84f652850dca8a1b578581e2fbb2cb95e791cc Mon Sep 17 00:00:00 2001 From: Charles Patel <17268094+acharles7@users.noreply.github.com> Date: Mon, 16 Oct 2023 07:24:16 -0500 Subject: [PATCH 585/700] Migrate mypy config to pyproject.toml (#3936) Co-authored-by: Charles Patel --- .gitignore | 1 + .pre-commit-config.yaml | 5 +++ mypy.ini | 46 ---------------------- pyproject.toml | 24 +++++++++++ scripts/check_pre_commit_rev_in_example.py | 2 +- scripts/check_version_in_basics_example.py | 2 +- scripts/diff_shades_gha_helper.py | 2 +- scripts/fuzz.py | 2 +- scripts/make_width_table.py | 2 +- src/blackd/__init__.py | 4 +- tests/optional.py | 2 +- tests/test_blackd.py | 2 +- 12 files changed, 40 insertions(+), 54 deletions(-) delete mode 100644 mypy.ini diff --git a/.gitignore b/.gitignore index 249499b135e..4a4f1b738ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ _build .DS_Store .vscode +.python-version docs/_static/pypi.svg .tox __pycache__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99b3565ed0e..623e661ac07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,7 @@ repos: hooks: - id: mypy exclude: ^docs/conf.py + args: ["--config-file", "pyproject.toml"] additional_dependencies: - types-PyYAML - tomli >= 0.2.6, < 2.0.0 @@ -51,6 +52,10 @@ repos: - platformdirs >= 2.1.0 - pytest - hypothesis + - aiohttp >= 3.7.4 + - types-commonmark + - urllib3 + - hypothesmith - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.0.3 diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index ad916185bce..00000000000 --- a/mypy.ini +++ /dev/null @@ -1,46 +0,0 @@ -[mypy] -# Specify the target platform details in config, so your developers are -# free to run mypy on Windows, Linux, or macOS and get consistent -# results. -python_version=3.8 - -mypy_path=src - -show_column_numbers=True -show_error_codes=True - -# be strict -strict=True - -# except for... -no_implicit_reexport = False - -# Unreachable blocks have been an issue when compiling mypyc, let's try -# to avoid 'em in the first place. -warn_unreachable=True - -[mypy-blib2to3.driver.*] -ignore_missing_imports = True - -[mypy-IPython.*] -ignore_missing_imports = True - -[mypy-colorama.*] -ignore_missing_imports = True - -[mypy-pathspec.*] -ignore_missing_imports = True - -[mypy-tokenize_rt.*] -ignore_missing_imports = True - -[mypy-uvloop.*] -ignore_missing_imports = True - -[mypy-_black_version.*] -ignore_missing_imports = True - -# CI only checks src/, but in case users are running LSP or similar we explicitly ignore -# errors in test data files. -[mypy-tests.data.*] -ignore_errors = True diff --git a/pyproject.toml b/pyproject.toml index d246eb0b272..8c55076e4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,7 @@ exclude = [ # Compiled modules can't be run directly and that's a problem here: "/src/black/__main__.py", ] +mypy-args = ["--ignore-missing-imports"] options = { debug_level = "0" } [tool.cibuildwheel] @@ -223,3 +224,26 @@ omit = [ ] [tool.coverage.run] relative_files = true + +[tool.mypy] +# Specify the target platform details in config, so your developers are +# free to run mypy on Windows, Linux, or macOS and get consistent +# results. +python_version = "3.8" +mypy_path = "src" +strict = true +# Unreachable blocks have been an issue when compiling mypyc, let's try to avoid 'em in the first place. +warn_unreachable = true +implicit_reexport = true +show_error_codes = true +show_column_numbers = true + +[[tool.mypy.overrides]] +module = ["pathspec.*", "IPython.*", "colorama.*", "tokenize_rt.*", "uvloop.*", "_black_version.*"] +ignore_missing_imports = true + +# CI only checks src/, but in case users are running LSP or similar we explicitly ignore +# errors in test data files. +[[tool.mypy.overrides]] +module = ["tests.data.*"] +ignore_errors = true diff --git a/scripts/check_pre_commit_rev_in_example.py b/scripts/check_pre_commit_rev_in_example.py index 9560b3b8401..107c6444dca 100644 --- a/scripts/check_pre_commit_rev_in_example.py +++ b/scripts/check_pre_commit_rev_in_example.py @@ -14,7 +14,7 @@ import commonmark import yaml -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup # type: ignore[import] def main(changes: str, source_version_control: str) -> None: diff --git a/scripts/check_version_in_basics_example.py b/scripts/check_version_in_basics_example.py index 7f559b3aee1..0f42bafe334 100644 --- a/scripts/check_version_in_basics_example.py +++ b/scripts/check_version_in_basics_example.py @@ -8,7 +8,7 @@ import sys import commonmark -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup # type: ignore[import] def main(changes: str, the_basics: str) -> None: diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 7a58fbe9b28..895516deb51 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -119,7 +119,7 @@ def main() -> None: @main.command("config", help="Acquire run configuration and metadata.") @click.argument("event", type=click.Choice(["push", "pull_request"])) def config(event: Literal["push", "pull_request"]) -> None: - import diff_shades + import diff_shades # type: ignore[import] if event == "push": jobs = [{"mode": "preview-changes", "force-flag": "--force-preview-style"}] diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 0c507381d92..929d3eac4f5 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -80,7 +80,7 @@ def test_idempotent_any_syntatically_valid_python( try: import sys - import atheris + import atheris # type: ignore[import] except ImportError: pass else: diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 061fdc8d95d..3c7cae60f7f 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -20,7 +20,7 @@ from os.path import basename, dirname, join from typing import Iterable, Tuple -import wcwidth +import wcwidth # type: ignore[import] def make_width_table() -> Iterable[Tuple[int, int, int]]: diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 6b0f3d33295..972f24181cb 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -74,7 +74,9 @@ def main(bind_host: str, bind_port: int) -> None: app = make_app() ver = black.__version__ black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}") - web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) + # TODO: aiohttp had an incorrect annotation for `print` argument, + # It'll be fixed once aiohttp releases that code + web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) # type: ignore[arg-type] def make_app() -> web.Application: diff --git a/tests/optional.py b/tests/optional.py index 8a39cc440a6..3f5277b6b03 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -26,7 +26,7 @@ from pytest import StashKey except ImportError: # pytest < 7 - from _pytest.store import StoreKey as StashKey # type: ignore[no-redef] + from _pytest.store import StoreKey as StashKey # type: ignore[import, no-redef] log = logging.getLogger(__name__) diff --git a/tests/test_blackd.py b/tests/test_blackd.py index c0152de73e6..59703036dc0 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -31,7 +31,7 @@ def unittest_run_loop(func, *args, **kwargs): @pytest.mark.blackd -class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] +class BlackDTestCase(AioHTTPTestCase): def test_blackd_main(self) -> None: with patch("blackd.web.run_app"): result = CliRunner().invoke(blackd.main, []) From 1648ac51806d092c95cb9bb2e4a5bffda6095bc1 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Mon, 16 Oct 2023 17:08:21 +0300 Subject: [PATCH 586/700] Fix long lines with power operator(s) getting splitted before line length (#3942) Fixes #3889 --- CHANGES.md | 1 + src/black/linegen.py | 21 ++++- src/black/mode.py | 1 + tests/data/cases/power_op_spacing.py | 18 ++++ tests/data/cases/preview_power_op_spacing.py | 97 ++++++++++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/preview_power_op_spacing.py diff --git a/CHANGES.md b/CHANGES.md index a608551815f..d1c4a075c32 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Fix long lines with power operators getting splitted before the line length (#3942) - Long type hints are now wrapped in parentheses and properly indented when split across multiple lines (#3899) - Magic trailing commas are now respected in return types. (#3916) diff --git a/src/black/linegen.py b/src/black/linegen.py index faeb3ba664c..d12ca39d037 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -536,6 +536,17 @@ def __post_init__(self) -> None: self.visit_case_block = self.visit_match_case +def _hugging_power_ops_line_to_string( + line: Line, + features: Collection[Feature], + mode: Mode, +) -> Optional[str]: + try: + return line_to_string(next(hug_power_op(line, features, mode))) + except CannotTransform: + return None + + def transform_line( line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: @@ -551,6 +562,14 @@ def transform_line( line_str = line_to_string(line) + # We need the line string when power operators are hugging to determine if we should + # split the line. Default to line_str, if no power operator are present on the line. + line_str_hugging_power_ops = ( + (_hugging_power_ops_line_to_string(line, features, mode) or line_str) + if Preview.fix_power_op_line_length in mode + else line_str + ) + ll = mode.line_length sn = mode.string_normalization string_merge = StringMerger(ll, sn) @@ -564,7 +583,7 @@ def transform_line( and not line.should_split_rhs and not line.magic_trailing_comma and ( - is_line_short_enough(line, mode=mode, line_str=line_str) + is_line_short_enough(line, mode=mode, line_str=line_str_hugging_power_ops) or line.contains_unsplittable_type_ignore() ) and not (line.inside_brackets and line.contains_standalone_comments()) diff --git a/src/black/mode.py b/src/black/mode.py index baf886abb7f..a57fa373568 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -188,6 +188,7 @@ class Preview(Enum): dummy_implementations = auto() walrus_subscript = auto() module_docstring_newlines = auto() + fix_power_op_line_length = auto() class Deprecated(UserWarning): diff --git a/tests/data/cases/power_op_spacing.py b/tests/data/cases/power_op_spacing.py index c95fa788fc3..b3ef0aae084 100644 --- a/tests/data/cases/power_op_spacing.py +++ b/tests/data/cases/power_op_spacing.py @@ -29,6 +29,13 @@ def function_dont_replace_spaces(): p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] r = x**y +s = 1 ** 1 +t = ( + 1 + ** 1 + **1 + ** 1 +) a = 5.0**~4.0 b = 5.0 ** f() @@ -47,6 +54,13 @@ def function_dont_replace_spaces(): 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)] +s = 1.0 ** 1.0 +t = ( + 1.0 + ** 1.0 + **1.0 + ** 1.0 +) # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) @@ -97,6 +111,8 @@ def function_dont_replace_spaces(): p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] r = x**y +s = 1**1 +t = 1**1**1**1 a = 5.0**~4.0 b = 5.0 ** f() @@ -115,6 +131,8 @@ def function_dont_replace_spaces(): 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)] +s = 1.0**1.0 +t = 1.0**1.0**1.0**1.0 # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) diff --git a/tests/data/cases/preview_power_op_spacing.py b/tests/data/cases/preview_power_op_spacing.py new file mode 100644 index 00000000000..650c6fecb20 --- /dev/null +++ b/tests/data/cases/preview_power_op_spacing.py @@ -0,0 +1,97 @@ +# flags: --preview +a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 +b = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 +c = 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 +d = 1**1 ** 1**1 ** 1**1 ** 1**1 ** 1**1**1 ** 1 ** 1**1 ** 1**1**1**1**1 ** 1 ** 1**1**1 **1**1** 1 ** 1 ** 1 +e = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟 +f = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟 + +a = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0 +b = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0 +c = 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 +d = 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0**1.0 ** 1.0 ** 1.0**1.0 ** 1.0**1.0**1.0 + +# output +a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 +b = ( + 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 + ** 1 +) +c = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 +d = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 +e = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟 +f = ( + 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 + ** 𨉟 +) + +a = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0 +b = ( + 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 + ** 1.0 +) +c = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0 +d = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0 From abe57e3d92727f1b26c717fad3978159b987fe79 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 16 Oct 2023 10:51:51 -0700 Subject: [PATCH 587/700] Treat raw strings like other docstrings (#3947) Fixes #3944 --- CHANGES.md | 1 + src/black/lines.py | 15 ++++++++++----- src/black/mode.py | 1 + tests/data/raw_docstring.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/data/raw_docstring.py diff --git a/CHANGES.md b/CHANGES.md index d1c4a075c32..1f6a008d643 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ multiple lines (#3899) - Magic trailing commas are now respected in return types. (#3916) - Require one empty line after module-level docstrings. (#3932) +- Treat raw triple-quoted strings as docstrings (#3947) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 14754d7532f..48fde888208 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -193,11 +193,16 @@ def is_class_paren_empty(self) -> bool: @property def is_triple_quoted_string(self) -> bool: """Is the line a triple quoted string?""" - return ( - bool(self) - and self.leaves[0].type == token.STRING - and self.leaves[0].value.startswith(('"""', "'''")) - ) + if not self or self.leaves[0].type != token.STRING: + return False + value = self.leaves[0].value + if value.startswith(('"""', "'''")): + return True + if Preview.accept_raw_docstrings in self.mode and value.startswith( + ("r'''", 'r"""', "R'''", 'R"""') + ): + return True + return False @property def opens_block(self) -> bool: diff --git a/src/black/mode.py b/src/black/mode.py index a57fa373568..309f22dae94 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -188,6 +188,7 @@ class Preview(Enum): dummy_implementations = auto() walrus_subscript = auto() module_docstring_newlines = auto() + accept_raw_docstrings = auto() fix_power_op_line_length = auto() diff --git a/tests/data/raw_docstring.py b/tests/data/raw_docstring.py new file mode 100644 index 00000000000..751fd3201df --- /dev/null +++ b/tests/data/raw_docstring.py @@ -0,0 +1,32 @@ +# flags: --preview --skip-string-normalization +class C: + + r"""Raw""" + +def f(): + + r"""Raw""" + +class SingleQuotes: + + + r'''Raw''' + +class UpperCaseR: + R"""Raw""" + +# output +class C: + r"""Raw""" + + +def f(): + r"""Raw""" + + +class SingleQuotes: + r'''Raw''' + + +class UpperCaseR: + R"""Raw""" From 722735d20ebdc66c0da0e0df7658293455694500 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 16 Oct 2023 10:53:38 -0700 Subject: [PATCH 588/700] Fix grammar for type alias support (#3949) Fixes #3948 --- CHANGES.md | 3 +++ src/blib2to3/Grammar.txt | 2 +- tests/data/cases/type_aliases.py | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1f6a008d643..610a9de0e43 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,9 @@ +- Add support for PEP 695 type aliases containing lambdas and other unusual expressions + (#3949) + ### Performance diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index be91df75740..5db78723cec 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -108,7 +108,7 @@ dotted_as_names: dotted_as_name (',' dotted_as_name)* dotted_name: NAME ('.' NAME)* global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* assert_stmt: 'assert' test [',' test] -type_stmt: "type" NAME [typeparams] '=' expr +type_stmt: "type" NAME [typeparams] '=' test compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt async_stmt: ASYNC (funcdef | with_stmt | for_stmt) diff --git a/tests/data/cases/type_aliases.py b/tests/data/cases/type_aliases.py index a3c1931c9fc..9631bfd5ccc 100644 --- a/tests/data/cases/type_aliases.py +++ b/tests/data/cases/type_aliases.py @@ -1,6 +1,10 @@ # flags: --minimum-version=3.12 + type A=int type Gen[T]=list[T] +type Alias[T]=lambda: T +type And[T]=T and T +type IfElse[T]=T if T else T type = aliased print(type(42)) @@ -9,6 +13,9 @@ type A = int type Gen[T] = list[T] +type Alias[T] = lambda: T +type And[T] = T and T +type IfElse[T] = T if T else T type = aliased print(type(42)) From bb588073ab286a9f1f8d839ab2cebe13011dd22c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 17 Oct 2023 00:59:15 -0700 Subject: [PATCH 589/700] Fix parser bug where "type" was misinterpreted as a keyword inside a match (#3950) Fixes #3790 Slightly hacky, but I think this is correct and it should also improve performance somewhat. --- CHANGES.md | 2 ++ src/blib2to3/pgen2/parse.py | 19 ++++++++++++++++++- tests/data/cases/pattern_matching_complex.py | 4 ++++ tests/data/cases/type_aliases.py | 9 +++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 610a9de0e43..f89b1b9df0a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,8 @@ +- Fix bug where attributes named `type` were not acccepted inside `match` statements + (#3950) - Add support for PEP 695 type aliases containing lambdas and other unusual expressions (#3949) diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 299cc24a15f..ad51a3dad08 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -211,6 +211,7 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None: # See note in docstring above. TL;DR this is ignored. self.convert = convert or lam_sub self.is_backtracking = False + self.last_token: Optional[int] = None def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: """Prepare for parsing. @@ -236,6 +237,7 @@ def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None: self.rootnode: Optional[NL] = None self.used_names: Set[str] = set() self.proxy = proxy + self.last_token = None def addtoken(self, type: int, value: str, context: Context) -> bool: """Add a token; return True iff this is the end of the program.""" @@ -317,6 +319,7 @@ def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> boo dfa, state, node = self.stack[-1] states, first = dfa # Done with this token + self.last_token = type return False else: @@ -343,9 +346,23 @@ def classify(self, type: int, value: str, context: Context) -> List[int]: return [self.grammar.keywords[value]] elif value in self.grammar.soft_keywords: assert type in self.grammar.tokens + # Current soft keywords (match, case, type) can only appear at the + # beginning of a statement. So as a shortcut, don't try to treat them + # like keywords in any other context. + # ('_' is also a soft keyword in the real grammar, but for our grammar + # it's just an expression, so we don't need to treat it specially.) + if self.last_token not in ( + None, + token.INDENT, + token.DEDENT, + token.NEWLINE, + token.SEMI, + token.COLON, + ): + return [self.grammar.tokens[type]] return [ - self.grammar.soft_keywords[value], self.grammar.tokens[type], + self.grammar.soft_keywords[value], ] ilabel = self.grammar.tokens.get(type) diff --git a/tests/data/cases/pattern_matching_complex.py b/tests/data/cases/pattern_matching_complex.py index b4355c7333a..10b4d26e289 100644 --- a/tests/data/cases/pattern_matching_complex.py +++ b/tests/data/cases/pattern_matching_complex.py @@ -143,3 +143,7 @@ y = 1 case []: y = 2 +# issue 3790 +match (X.type, Y): + case _: + pass diff --git a/tests/data/cases/type_aliases.py b/tests/data/cases/type_aliases.py index 9631bfd5ccc..7c2009e8202 100644 --- a/tests/data/cases/type_aliases.py +++ b/tests/data/cases/type_aliases.py @@ -5,6 +5,8 @@ type Alias[T]=lambda: T type And[T]=T and T type IfElse[T]=T if T else T +type One = int; type Another = str +class X: type InClass = int type = aliased print(type(42)) @@ -16,6 +18,13 @@ type Alias[T] = lambda: T type And[T] = T and T type IfElse[T] = T if T else T +type One = int +type Another = str + + +class X: + type InClass = int + type = aliased print(type(42)) From 9edba85f71d50d12996ef7bda576426362016171 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 17 Oct 2023 07:22:24 -0700 Subject: [PATCH 590/700] Prepare release 23.10.0 (#3951) --- CHANGES.md | 60 +++++++++++++-------- docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f89b1b9df0a..2a50e456550 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,25 +10,14 @@ -- Fix comments getting removed from inside parenthesized strings (#3909) - ### Preview style -- Fix long lines with power operators getting splitted before the line length (#3942) -- Long type hints are now wrapped in parentheses and properly indented when split across - multiple lines (#3899) -- Magic trailing commas are now respected in return types. (#3916) -- Require one empty line after module-level docstrings. (#3932) -- Treat raw triple-quoted strings as docstrings (#3947) - ### Configuration -- Fix cache versioning logic when `BLACK_CACHE_DIR` is set (#3937) - ### Packaging @@ -37,11 +26,6 @@ -- Fix bug where attributes named `type` were not acccepted inside `match` statements - (#3950) -- Add support for PEP 695 type aliases containing lambdas and other unusual expressions - (#3949) - ### Performance @@ -50,11 +34,6 @@ -- Black no longer attempts to provide special errors for attempting to format Python 2 - code (#3933) -- Black will more consistently print stacktraces on internal errors in verbose mode - (#3938) - ### _Blackd_ @@ -63,13 +42,48 @@ -- The action output displayed in the job summary is now wrapped in Markdown (#3914) - ### Documentation +## 23.10.0 + +### Stable style + +- Fix comments getting removed from inside parenthesized strings (#3909) + +### Preview style + +- Fix long lines with power operators getting split before the line length (#3942) +- Long type hints are now wrapped in parentheses and properly indented when split across + multiple lines (#3899) +- Magic trailing commas are now respected in return types. (#3916) +- Require one empty line after module-level docstrings. (#3932) +- Treat raw triple-quoted strings as docstrings (#3947) + +### Configuration + +- Fix cache versioning logic when `BLACK_CACHE_DIR` is set (#3937) + +### Parser + +- Fix bug where attributes named `type` were not acccepted inside `match` statements + (#3950) +- Add support for PEP 695 type aliases containing lambdas and other unusual expressions + (#3949) + +### Output + +- Black no longer attempts to provide special errors for attempting to format Python 2 + code (#3933) +- Black will more consistently print stacktraces on internal errors in verbose mode + (#3938) + +### Integrations + +- The action output displayed in the job summary is now wrapped in Markdown (#3914) + ## 23.9.1 Due to various issues, the previous release (23.9.0) did not include compiled mypyc diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 2afcc02f3cd..16354f849ba 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black-jupyter # 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 36119f225e6..5b132a95eae 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -194,8 +194,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.9.1 (compiled: yes) -$ black --required-version 23.9.1 -c "format = 'this'" +black, 23.10.0 (compiled: yes) +$ black --required-version 23.10.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -286,7 +286,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.9.1 +black, 23.10.0 ``` #### `--config` From 882d8795c6ff65c02f2657e596391748d1b6b7f5 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Fri, 20 Oct 2023 06:09:33 +0300 Subject: [PATCH 591/700] Fix merging implicit multiline strings that have inline comments (#3956) * Fix test behaviour * Add new test cases * Skip merging strings that have inline comments * Don't merge lines with multiline strings with inline comments * Changelog entry * Document implicit multiline string merging rules * Fix PR number --- CHANGES.md | 2 +- docs/the_black_code_style/future_style.md | 64 +++++++++++++++++++ src/black/linegen.py | 1 + src/black/lines.py | 15 +++++ src/black/trans.py | 14 +++- .../cases/preview_long_strings__regression.py | 6 +- tests/data/cases/preview_multiline_strings.py | 28 ++++++++ 7 files changed, 125 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2a50e456550..79b5c6034e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ ### Preview style - +- Fix merging implicit multiline strings that have inline comments (#3956) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 861bb64bff4..367ff98537c 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -160,3 +160,67 @@ MULTILINE = """ foobar """.replace("\n", "") ``` + +Implicit multiline strings are special, because they can have inline comments. Strings +without comments are merged, for example + +```python +s = ( + "An " + "implicit " + "multiline " + "string" +) +``` + +becomes + +```python +s = "An implicit multiline string" +``` + +A comment on any line of the string (or between two string lines) will block the +merging, so + +```python +s = ( + "An " # Important comment concerning just this line + "implicit " + "multiline " + "string" +) +``` + +and + +```python +s = ( + "An " + "implicit " + # Comment in between + "multiline " + "string" +) +``` + +will not be merged. Having the comment after or before the string lines (but still +inside the parens) will merge the string. For example + +```python +s = ( # Top comment + "An " + "implicit " + "multiline " + "string" + # Bottom comment +) +``` + +becomes + +```python +s = ( # Top comment + "An implicit multiline string" + # Bottom comment +) +``` diff --git a/src/black/linegen.py b/src/black/linegen.py index d12ca39d037..2bfe587fa0e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -587,6 +587,7 @@ def transform_line( or line.contains_unsplittable_type_ignore() ) and not (line.inside_brackets and line.contains_standalone_comments()) + and not line.contains_implicit_multiline_string_with_comments() ): # Only apply basic string preprocessing, since lines shouldn't be split here. if Preview.string_processing in mode: diff --git a/src/black/lines.py b/src/black/lines.py index 48fde888208..6acc95e7a7e 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -239,6 +239,21 @@ def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool: return False + def contains_implicit_multiline_string_with_comments(self) -> bool: + """Chck if we have an implicit multiline string with comments on the line""" + for leaf_type, leaf_group_iterator in itertools.groupby( + self.leaves, lambda leaf: leaf.type + ): + if leaf_type != token.STRING: + continue + leaf_list = list(leaf_group_iterator) + if len(leaf_list) == 1: + continue + for leaf in leaf_list: + if self.comments_after(leaf): + return True + return False + def contains_uncollapsable_type_comments(self) -> bool: ignored_ids = set() try: diff --git a/src/black/trans.py b/src/black/trans.py index a2bff7f227a..a3f6467cc9e 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -390,7 +390,19 @@ def do_match(self, line: Line) -> TMatchResult: and is_valid_index(idx + 1) and LL[idx + 1].type == token.STRING ): - if not is_part_of_annotation(leaf): + # Let's check if the string group contains an inline comment + # If we have a comment inline, we don't merge the strings + contains_comment = False + i = idx + while is_valid_index(i): + if LL[i].type != token.STRING: + break + if line.comments_after(LL[i]): + contains_comment = True + break + i += 1 + + if not is_part_of_annotation(leaf) and not contains_comment: string_indices.append(idx) # Advance to the next non-STRING leaf. diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 40d5e745cc8..436157f4e05 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -210,8 +210,8 @@ def foo(): some_tuple = ("some string", "some string" " which should be joined") -some_commented_string = ( - "This string is long but not so long that it needs hahahah toooooo be so greatttt" # This comment gets thrown to the top. +some_commented_string = ( # This comment stays at the top. + "This string is long but not so long that it needs hahahah toooooo be so greatttt" " {} that I just can't think of any more good words to say about it at" " allllllllllll".format("ha") # comments here are fine ) @@ -834,7 +834,7 @@ def foo(): some_tuple = ("some string", "some string which should be joined") -some_commented_string = ( # This comment gets thrown to the top. +some_commented_string = ( # This comment stays at the top. "This string is long but not so long that it needs hahahah toooooo be so greatttt" " {} that I just can't think of any more good words to say about it at" " allllllllllll".format("ha") # comments here are fine diff --git a/tests/data/cases/preview_multiline_strings.py b/tests/data/cases/preview_multiline_strings.py index dec4ef2e548..3ff643610b7 100644 --- a/tests/data/cases/preview_multiline_strings.py +++ b/tests/data/cases/preview_multiline_strings.py @@ -157,6 +157,24 @@ def dastardly_default_value( `--global-option` is reserved to flags like `--verbose` or `--quiet`. """ +this_will_become_one_line = ( + "a" + "b" + "c" +) + +this_will_stay_on_three_lines = ( + "a" # comment + "b" + "c" +) + +this_will_also_become_one_line = ( # comment + "a" + "b" + "c" +) + # output """cow say""", @@ -357,3 +375,13 @@ def dastardly_default_value( Please use `--build-option` instead, `--global-option` is reserved to flags like `--verbose` or `--quiet`. """ + +this_will_become_one_line = "abc" + +this_will_stay_on_three_lines = ( + "a" # comment + "b" + "c" +) + +this_will_also_become_one_line = "abc" # comment From 0a37888e79059018eef9293a724b14da59d3377a Mon Sep 17 00:00:00 2001 From: Aniket Patil <128228805+AniketP04@users.noreply.github.com> Date: Mon, 23 Oct 2023 02:46:43 +0530 Subject: [PATCH 592/700] Fix typos in CHANGES.md (#3963) --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 79b5c6034e8..a75b54d8d81 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -68,7 +68,7 @@ ### Parser -- Fix bug where attributes named `type` were not acccepted inside `match` statements +- Fix bug where attributes named `type` were not accepted inside `match` statements (#3950) - Add support for PEP 695 type aliases containing lambdas and other unusual expressions (#3949) @@ -926,7 +926,7 @@ and the first release covered by our new [`master`](https://github.com/psf/black/tree/main) branch with the [`main`](https://github.com/psf/black/tree/main) branch. Some additional changes in the source code were also made. (#2210) -- Sigificantly reorganized the documentation to make much more sense. Check them out by +- Significantly reorganized the documentation to make much more sense. Check them out by heading over to [the stable docs on RTD](https://black.readthedocs.io/en/stable/). (#2174) From 2db5ab0a7b3b321e4cec70964239ee88087cd810 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Mon, 23 Oct 2023 17:38:36 +0300 Subject: [PATCH 593/700] Allow empty line after block open before a comment or compound statement (#3967) --- CHANGES.md | 1 + src/black/lines.py | 27 ++++- src/black/mode.py | 1 + src/black/nodes.py | 4 + ...allow_empty_first_line_in_special_cases.py | 106 ++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/preview_allow_empty_first_line_in_special_cases.py diff --git a/CHANGES.md b/CHANGES.md index a75b54d8d81..86e820a6fd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ ### Preview style - Fix merging implicit multiline strings that have inline comments (#3956) +- Allow empty first line after block open before a comment or compound statement (#3967) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 6acc95e7a7e..a73c429e3d9 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -24,6 +24,8 @@ STANDALONE_COMMENT, TEST_DESCENDANTS, child_towards, + is_docstring, + is_funcdef, is_import, is_multiline_string, is_one_sequence_between, @@ -686,7 +688,30 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 - if self.previous_line and self.previous_line.opens_block: + is_empty_first_line_ok = ( + Preview.allow_empty_first_line_before_new_block_or_comment + in current_line.mode + and ( + # If it's a standalone comment + current_line.leaves[0].type == STANDALONE_COMMENT + # If it opens a new block + or current_line.opens_block + # If it's a triple quote comment (but not at the start of a funcdef) + or ( + is_docstring(current_line.leaves[0]) + and self.previous_line + and self.previous_line.leaves[0] + and self.previous_line.leaves[0].parent + and not is_funcdef(self.previous_line.leaves[0].parent) + ) + ) + ) + + if ( + self.previous_line + and self.previous_line.opens_block + and not is_empty_first_line_ok + ): return 0, 0 return before, 0 diff --git a/src/black/mode.py b/src/black/mode.py index 309f22dae94..4effeef3e7c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -190,6 +190,7 @@ class Preview(Enum): module_docstring_newlines = auto() accept_raw_docstrings = auto() fix_power_op_line_length = auto() + allow_empty_first_line_before_new_block_or_comment = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index edd201a21e9..b2e96cb9edf 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -718,6 +718,10 @@ def is_multiline_string(leaf: Leaf) -> bool: return has_triple_quotes(leaf.value) and "\n" in leaf.value +def is_funcdef(node: Node) -> bool: + return node.type == syms.funcdef + + def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" diff --git a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py b/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py new file mode 100644 index 00000000000..96c1433c110 --- /dev/null +++ b/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py @@ -0,0 +1,106 @@ +# flags: --preview +def foo(): + """ + Docstring + """ + + # Here we go + if x: + + # This is also now fine + a = 123 + + else: + # But not necessary + a = 123 + + if y: + + while True: + + """ + Long comment here + """ + a = 123 + + if z: + + for _ in range(100): + a = 123 + else: + + try: + + # this should be ok + a = 123 + except: + + """also this""" + a = 123 + + +def bar(): + + if x: + a = 123 + + +def baz(): + + # OK + if x: + a = 123 + +# output + +def foo(): + """ + Docstring + """ + + # Here we go + if x: + + # This is also now fine + a = 123 + + else: + # But not necessary + a = 123 + + if y: + + while True: + + """ + Long comment here + """ + a = 123 + + if z: + + for _ in range(100): + a = 123 + else: + + try: + + # this should be ok + a = 123 + except: + + """also this""" + a = 123 + + +def bar(): + + if x: + a = 123 + + +def baz(): + + # OK + if x: + a = 123 From 7f1c578b89b2c256a6ce3b70fc1b970b3ffa3349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:42:49 -0700 Subject: [PATCH 594/700] Bump peter-evans/create-or-update-comment from 3.0.2 to 3.1.0 (#3966) Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 3.0.2 to 3.1.0. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/c6c9a1a66007646a28c153e2a8580a5bad27bcfa...23ff15729ef2fc348714a3bb66d2f655ca9066f2) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment 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> --- .github/workflows/diff_shades_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index b86bd93410e..49fd376d85e 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -41,7 +41,7 @@ jobs: - name: Create or update PR comment if: steps.metadata.outputs.needs-comment == 'true' - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.metadata.outputs.pr-number }} From d291c2338c3c1feee4f3f26985c0964ec1b7eb9f Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 23 Oct 2023 08:36:47 -0700 Subject: [PATCH 595/700] Move Docker image to hatch + compile (#3965) --- CHANGES.md | 2 ++ Dockerfile | 12 +++++++----- docs/usage_and_configuration/black_docker_image.md | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 86e820a6fd9..fe0b2ebb9d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ +- Change Dockerfile to hatch + compile black (#3965) + ### Parser diff --git a/Dockerfile b/Dockerfile index a9e0ea5081e..bfd9acccb99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,14 @@ FROM python:3.11-slim AS builder RUN mkdir /src COPY . /src/ ENV VIRTUAL_ENV=/opt/venv +ENV HATCH_BUILD_HOOKS_ENABLE=1 +# Install build tools to compile black + dependencies +RUN apt update && apt install -y build-essential git python3-dev RUN python -m venv $VIRTUAL_ENV -RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setuptools wheel \ - # Install build tools to compile dependencies that don't have prebuilt wheels - && apt update && apt install -y git build-essential \ - && cd /src \ - && pip install --no-cache-dir .[colorama,d] +RUN python -m pip install --no-cache-dir hatch hatch-fancy-pypi-readme hatch-vcs +RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setuptools \ + && cd /src && hatch build -t wheel \ + && pip install --no-cache-dir dist/*-cp*[colorama,d,uvloop] FROM python:3.11-slim diff --git a/docs/usage_and_configuration/black_docker_image.md b/docs/usage_and_configuration/black_docker_image.md index 85aec91ef1c..c97c25af328 100644 --- a/docs/usage_and_configuration/black_docker_image.md +++ b/docs/usage_and_configuration/black_docker_image.md @@ -24,6 +24,8 @@ created for all unreleased [commits on the `main` branch](https://github.com/psf/black/commits/main). This tag is not meant to be used by external users. +From version 23.11.0 the Docker image installs a compiled black into the image. + ## Usage A permanent container doesn't have to be created to use _Black_ as a Docker image. It's From a7643fac8d97c15640a2b1a79f68b3dc771aebfb Mon Sep 17 00:00:00 2001 From: Dario Curreri <48800335+dariocurr@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:40:09 +0200 Subject: [PATCH 596/700] Add summary parameter to action (#3958) --- CHANGES.md | 3 ++- action.yml | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fe0b2ebb9d8..89837c8f545 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,7 +43,8 @@ ### Integrations - +- The summary output for GitHub workflows is now suppressible using the `summary` + parameter. (#3958) ### Documentation diff --git a/action.yml b/action.yml index 8b698ae3c80..a22005ac243 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,10 @@ inputs: description: 'Python Version specifier (PEP440) - e.g. "21.5b1"' required: false default: "" + summary: + description: "Whether to add the output to the workflow summary" + required: false + default: true branding: color: "black" icon: "check-circle" @@ -47,10 +51,12 @@ runs: # Display the raw output in the step echo "${out}" - # Display the Markdown output in the job summary - echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY - echo "${out}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.summary }}" == "true" ]; then + # Display the Markdown output in the job summary + echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY + echo "${out}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + fi # Exit with the exit-code returned by Black exit ${exit_code} From c0adca321dc0d97a704de8ed0353e5b894a6a912 Mon Sep 17 00:00:00 2001 From: William Moreno Date: Mon, 23 Oct 2023 11:21:58 -0600 Subject: [PATCH 597/700] docs: specifies the use of the .git-blame-ignore-revs file (#3961) --- docs/guides/introducing_black_to_your_project.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md index 71a566fbda1..3927eb1a386 100644 --- a/docs/guides/introducing_black_to_your_project.md +++ b/docs/guides/introducing_black_to_your_project.md @@ -18,7 +18,8 @@ previous revision that modified those lines. So when migrating your project's code style to _Black_, reformat everything and commit the changes (preferably in one massive commit). Then put the full 40 characters commit -identifier(s) into a file. +identifier(s) into a file usually called `.git-blame-ignore-revs` at the root of your +project directory. ```text # Migrate code style to Black From 8de4be516879302afce542ac80a6a43ced807759 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Tue, 24 Oct 2023 02:37:14 +0900 Subject: [PATCH 598/700] Fix CI failing (#3957) * Fix CI failing * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs: update CHANGES.md * docs: fix changelog location to unreleased --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGES.md | 2 ++ action.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 89837c8f545..7e1ed79cd48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,6 +46,8 @@ - The summary output for GitHub workflows is now suppressible using the `summary` parameter. (#3958) +- Fix the action failing when Black check doesn't pass (#3957) + ### Documentation - -### Stable style - - +- Maintanence release to get a fix out for GitHub Action edge case (#3957) ### Preview style - Fix merging implicit multiline strings that have inline comments (#3956) - Allow empty first line after block open before a comment or compound statement (#3967) -### Configuration - - - ### Packaging - - - Change Dockerfile to hatch + compile black (#3965) -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - ### Integrations - The summary output for GitHub workflows is now suppressible using the `summary` parameter. (#3958) - - Fix the action failing when Black check doesn't pass (#3957) ### Documentation - +- It is known Windows documentation CI is broken + https://github.com/psf/black/issues/3968 ## 23.10.0 diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 16354f849ba..597a6b993c7 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black-jupyter # 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 5b132a95eae..f25dbb13d4d 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -194,8 +194,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.10.0 (compiled: yes) -$ black --required-version 23.10.0 -c "format = 'this'" +black, 23.10.1 (compiled: yes) +$ black --required-version 23.10.1 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -286,7 +286,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.10.0 +black, 23.10.1 ``` #### `--config` From ef1048d5f8205cb03358a6a373710c2a71d047b4 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 23 Oct 2023 23:26:40 -0700 Subject: [PATCH 600/700] Add Unreleased template to CHANGES.md (#3973) Add Unreleased template to CHANGES.md - Did this via tool working on in another branch --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1e90c12b4fb..c4ae056b1b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.10.1 ### Highlights From 1d4c31aa589dc0c8633af7531f8cc09192917b38 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Wed, 25 Oct 2023 18:35:37 +0300 Subject: [PATCH 601/700] [925] Improve multiline dictionary and list indentation for sole function parameter (#3964) --- CHANGES.md | 3 +- docs/the_black_code_style/future_style.md | 26 ++ src/black/linegen.py | 13 + src/black/mode.py | 1 + ..._parens_with_braces_and_square_brackets.py | 273 ++++++++++++++++++ .../cases/preview_long_strings__regression.py | 22 +- 6 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py diff --git a/CHANGES.md b/CHANGES.md index c4ae056b1b9..f7d02af187d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,8 @@ ### Preview style - +- Multiline dictionaries and lists that are the sole argument to a function are now + indented less (#3964) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 367ff98537c..e73c16ba26e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -113,6 +113,32 @@ my_dict = { } ``` +### Improved multiline dictionary and list indentation for sole function parameter + +For better readability and less verticality, _Black_ now pairs parantheses ("(", ")") +with braces ("{", "}") and square brackets ("[", "]") on the same line for single +parameter function calls. For example: + +```python +foo( + [ + 1, + 2, + 3, + ] +) +``` + +will be changed to: + +```python +foo([ + 1, + 2, + 3, +]) +``` + ### Improved multiline string handling _Black_ is smarter when formatting multiline strings, especially in function arguments, diff --git a/src/black/linegen.py b/src/black/linegen.py index 2bfe587fa0e..5f5a69152d5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -815,6 +815,19 @@ def _first_right_hand_split( tail_leaves.reverse() body_leaves.reverse() head_leaves.reverse() + + if Preview.hug_parens_with_braces_and_square_brackets in line.mode: + if ( + tail_leaves[0].type == token.RPAR + and tail_leaves[0].value + and tail_leaves[0].opening_bracket is head_leaves[-1] + and body_leaves[-1].type in [token.RBRACE, token.RSQB] + and body_leaves[-1].opening_bracket is body_leaves[0] + ): + head_leaves = head_leaves + body_leaves[:1] + tail_leaves = body_leaves[-1:] + tail_leaves + body_leaves = body_leaves[1:-1] + head = bracket_split_build_line( head_leaves, line, opening_bracket, component=_BracketSplitComponent.head ) diff --git a/src/black/mode.py b/src/black/mode.py index 4effeef3e7c..99b2a84a63d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -190,6 +190,7 @@ class Preview(Enum): module_docstring_newlines = auto() accept_raw_docstrings = auto() fix_power_op_line_length = auto() + hug_parens_with_braces_and_square_brackets = auto() allow_empty_first_line_before_new_block_or_comment = auto() diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py new file mode 100644 index 00000000000..98ed342fcbc --- /dev/null +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -0,0 +1,273 @@ +# flags: --preview +def foo_brackets(request): + return JsonResponse( + { + "var_1": foo, + "var_2": bar, + } + ) + +def foo_square_brackets(request): + return JsonResponse( + [ + "var_1", + "var_2", + ] + ) + +func({"a": 37, "b": 42, "c": 927, "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111}) + +func(["random_string_number_one","random_string_number_two","random_string_number_three","random_string_number_four"]) + +func( + { + # expand me + 'a':37, + 'b':42, + 'c':927 + } +) + +func( + [ + 'a', + 'b', + 'c', + ] +) + +func( # a + [ # b + "c", # c + "d", # d + "e", # e + ] # f +) # g + +func( # a + { # b + "c": 1, # c + "d": 2, # d + "e": 3, # e + } # f +) # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func( + [ # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + "c", + # preserve me but hug brackets + "d", + "e", + ] +) + +func( + [ + "c", + "d", + "e", + # preserve me but hug brackets + ] +) + +func( + [ + "c", + "d", + "e", + ] # preserve me but hug brackets +) + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([x for x in "long line long line long line long line long line long line long line"]) +func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) + +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +# output +def foo_brackets(request): + return JsonResponse({ + "var_1": foo, + "var_2": bar, + }) + + +def foo_square_brackets(request): + return JsonResponse([ + "var_1", + "var_2", + ]) + + +func({ + "a": 37, + "b": 42, + "c": 927, + "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111, +}) + +func([ + "random_string_number_one", + "random_string_number_two", + "random_string_number_three", + "random_string_number_four", +]) + +func({ + # expand me + "a": 37, + "b": 42, + "c": 927, +}) + +func([ + "a", + "b", + "c", +]) + +func([ # a # b + "c", # c + "d", # d + "e", # e +]) # f # g + +func({ # a # b + "c": 1, # c + "d": 2, # d + "e": 3, # e +}) # f # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func([ # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + "c", + # preserve me but hug brackets + "d", + "e", +]) + +func([ + "c", + "d", + "e", + # preserve me but hug brackets +]) + +func([ + "c", + "d", + "e", +]) # preserve me but hug brackets + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([ + x for x in "long line long line long line long line long line long line long line" +]) +func([ + x + for x in [ + x + for x in "long line long line long line long line long line long line long line" + ] +]) + +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({ + { + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", + } +}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 436157f4e05..313d898cd83 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -962,19 +962,17 @@ def who(self): ) -xxxxxxx_xxxxxx_xxxxxxx = xxx( - [ - xxxxxxxxxxxx( - xxxxxx_xxxxxxx=( - '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx =' - ' "xxxxxxxxxxxx")) && ' - # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx. - "(x.bbbbbbbbbbbb.xxx != " - '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && ' - ) +xxxxxxx_xxxxxx_xxxxxxx = xxx([ + xxxxxxxxxxxx( + xxxxxx_xxxxxxx=( + '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx =' + ' "xxxxxxxxxxxx")) && ' + # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx. + "(x.bbbbbbbbbbbb.xxx != " + '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && ' ) - ] -) + ) +]) if __name__ == "__main__": for i in range(4, 8): From 878937bcc3282319081057e2f1dbee5e24d69d68 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Wed, 25 Oct 2023 19:47:21 +0300 Subject: [PATCH 602/700] [2213] Add support for single line format skip with other comments on the same line (#3959) --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 12 ++-- src/black/__init__.py | 2 +- src/black/comments.py | 63 +++++++++++++++---- src/black/mode.py | 1 + ...line_format_skip_with_multiple_comments.py | 20 ++++++ 6 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py diff --git a/CHANGES.md b/CHANGES.md index f7d02af187d..c96186c93cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ ### Configuration - +- Add support for single line format skip with other comments on the same line (#3959) ### Packaging diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index ff757a8276b..f59c1853f72 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -8,12 +8,14 @@ deliberately limited and rarely added. Previous formatting is taken into account 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 lines that end with +_Black_ reformats entire files in place. It doesn't reformat lines that contain `# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. -`# fmt: on/off` must be on the same level of indentation and in the same block, meaning -no unindents beyond the initial indentation level between them. It also recognizes -[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a -courtesy for straddling code. +`# fmt: skip` can be mixed with other pragmas/comments either with multiple comments +(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g. +`# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation +and in the same block, meaning no unindents beyond the initial indentation level between +them. It also recognizes [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 diff --git a/src/black/__init__.py b/src/black/__init__.py index 188a4f79f0e..7cf93b89e42 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1099,7 +1099,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} if supports_feature(versions, feature) } - normalize_fmt_off(src_node) + normalize_fmt_off(src_node, mode) lines = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { diff --git a/src/black/comments.py b/src/black/comments.py index 226968bff98..862fc7607cc 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -3,6 +3,7 @@ from functools import lru_cache from typing import Final, Iterator, List, Optional, Union +from black.mode import Mode, Preview from black.nodes import ( CLOSING_BRACKETS, STANDALONE_COMMENT, @@ -20,10 +21,11 @@ FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} -FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} COMMENT_EXCEPTIONS = " !:#'" +_COMMENT_PREFIX = "# " +_COMMENT_LIST_SEPARATOR = ";" @dataclass @@ -130,14 +132,14 @@ def make_comment(content: str) -> str: return "#" + content -def normalize_fmt_off(node: Node) -> None: +def normalize_fmt_off(node: Node, mode: Mode) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node) + try_again = convert_one_fmt_off_pair(node, mode) -def convert_one_fmt_off_pair(node: Node) -> bool: +def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. @@ -145,21 +147,27 @@ def convert_one_fmt_off_pair(node: Node) -> bool: for leaf in node.leaves(): previous_consumed = 0 for comment in list_comments(leaf.prefix, is_endmarker=False): - if comment.value not in FMT_PASS: + should_pass_fmt = comment.value in FMT_OFF or _contains_fmt_skip_comment( + comment.value, mode + ) + if not should_pass_fmt: previous_consumed = comment.consumed continue # We only want standalone comments. If there's no previous leaf or # the previous leaf is indentation, it's a standalone comment in # disguise. - if comment.value in FMT_PASS and comment.type != STANDALONE_COMMENT: + if should_pass_fmt and comment.type != STANDALONE_COMMENT: prev = preceding_leaf(leaf) if prev: if comment.value in FMT_OFF and prev.type not in WHITESPACE: continue - if comment.value in FMT_SKIP and prev.type in WHITESPACE: + if ( + _contains_fmt_skip_comment(comment.value, mode) + and prev.type in WHITESPACE + ): continue - ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode)) if not ignored_nodes: continue @@ -168,7 +176,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: prefix = first.prefix if comment.value in FMT_OFF: first.prefix = prefix[comment.consumed :] - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): first.prefix = "" standalone_comment_prefix = prefix else: @@ -178,7 +186,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool: hidden_value = "".join(str(n) for n in ignored_nodes) if comment.value in FMT_OFF: hidden_value = comment.value + "\n" + hidden_value - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): hidden_value += " " + comment.value if hidden_value.endswith("\n"): # That happens when one of the `ignored_nodes` ended with a NEWLINE @@ -205,13 +213,15 @@ def convert_one_fmt_off_pair(node: Node) -> bool: return False -def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]: +def generate_ignored_nodes( + leaf: Leaf, comment: ProtoComment, mode: Mode +) -> Iterator[LN]: """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. If comment is skip, returns leaf only. Stops at the end of the block. """ - if comment.value in FMT_SKIP: + if _contains_fmt_skip_comment(comment.value, mode): yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment) return container: Optional[LN] = container_of(leaf) @@ -327,3 +337,32 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool: return True return False + + +def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: + """ + Checks if the given comment contains FMT_SKIP alone or paired with other comments. + Matching styles: + # fmt:skip <-- single comment + # noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview) + # pylint:XXX; fmt:skip <-- list of comments (; separated, Preview) + """ + semantic_comment_blocks = ( + [ + comment_line, + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.split(_COMMENT_PREFIX)[1:] + ], + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.strip(_COMMENT_PREFIX).split( + _COMMENT_LIST_SEPARATOR + ) + ], + ] + if Preview.single_line_format_skip_with_multiple_comments in mode + else [comment_line] + ) + + return any(comment in FMT_SKIP for comment in semantic_comment_blocks) diff --git a/src/black/mode.py b/src/black/mode.py index 99b2a84a63d..4e4effffb86 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -192,6 +192,7 @@ class Preview(Enum): fix_power_op_line_length = auto() hug_parens_with_braces_and_square_brackets = auto() allow_empty_first_line_before_new_block_or_comment = auto() + single_line_format_skip_with_multiple_comments = auto() class Deprecated(UserWarning): diff --git a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py b/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py new file mode 100644 index 00000000000..efde662baa8 --- /dev/null +++ b/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py @@ -0,0 +1,20 @@ +# flags: --preview +foo = 123 # fmt: skip # noqa: E501 # pylint +bar = ( + 123 , + ( 1 + 5 ) # pylint # fmt:skip +) +baz = "a" + "b" # pylint; fmt: skip; noqa: E501 +skip_will_not_work = "a" + "b" # pylint fmt:skip +skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it + +# output + +foo = 123 # fmt: skip # noqa: E501 # pylint +bar = ( + 123 , + ( 1 + 5 ) # pylint # fmt:skip +) +baz = "a" + "b" # pylint; fmt: skip; noqa: E501 +skip_will_not_work = "a" + "b" # pylint fmt:skip +skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it From f7174bfc431e22f38b502579d1234989c3c5ce15 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 27 Oct 2023 01:43:42 +0900 Subject: [PATCH 603/700] Fix typo in future_style.md (#3979) parantheses -> parentheses --- docs/the_black_code_style/future_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index e73c16ba26e..f2534b0f0d0 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -115,7 +115,7 @@ my_dict = { ### Improved multiline dictionary and list indentation for sole function parameter -For better readability and less verticality, _Black_ now pairs parantheses ("(", ")") +For better readability and less verticality, _Black_ now pairs parentheses ("(", ")") with braces ("{", "}") and square brackets ("[", "]") on the same line for single parameter function calls. For example: From de701fe6aa0d61526b806dd31610da5cf8b67ab9 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:13:25 -0700 Subject: [PATCH 604/700] Fix CI by running on Python 3.11 (#3984) aiohttp doesn't yet support 3.12 --- .github/workflows/diff_shades.yml | 4 ++-- .github/workflows/doc.yml | 2 +- .github/workflows/lint.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 97db907abc8..6bfc6ca9ed8 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install diff-shades and support dependencies run: | @@ -59,7 +59,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install diff-shades and support dependencies run: | diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fa3d87c70f5..9a23e19cadd 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3eaf5785f5a..7fe1b04eb02 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "*" + python-version: "3.11" - name: Install dependencies run: | From 7bfa35cca88a2a6b875fb8564c19164143a46f1d Mon Sep 17 00:00:00 2001 From: Surav Shrestha <148626286+shresthasurav@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:11:47 +0545 Subject: [PATCH 605/700] docs: fix typos in change log and documentations (#3985) --- CHANGES.md | 2 +- docs/the_black_code_style/current_style.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c96186c93cc..7703223a119 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,7 +52,7 @@ ### Highlights -- Maintanence release to get a fix out for GitHub Action edge case (#3957) +- Maintenance release to get a fix out for GitHub Action edge case (#3957) ### Preview style diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index f59c1853f72..431bae525f6 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -11,7 +11,7 @@ used by _Black_ can be viewed as a strict subset of PEP 8. _Black_ reformats entire files in place. It doesn't reformat lines that contain `# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. `# fmt: skip` can be mixed with other pragmas/comments either with multiple comments -(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g. +(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separated list (e.g. `# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation and in the same block, meaning no unindents beyond the initial indentation level between them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the From c369e446f9dbff313ebb555bf461b4e7778ca78d Mon Sep 17 00:00:00 2001 From: sth Date: Fri, 27 Oct 2023 09:43:51 +0200 Subject: [PATCH 606/700] Fix matching of absolute paths in `--include` (#3976) --- CHANGES.md | 2 ++ src/black/files.py | 2 +- tests/test_black.py | 59 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7703223a119..71f62d0e11f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ - Add support for single line format skip with other comments on the same line (#3959) +- Fix a bug in the matching of absolute path names in `--include` (#3976) + ### Packaging diff --git a/src/black/files.py b/src/black/files.py index 362898dc0fd..1eed7eda828 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -389,7 +389,7 @@ def gen_python_files( warn=verbose or not quiet ): continue - include_match = include.search(normalized_path) if include else True + include_match = include.search(root_relative_path) if include else True if include_match: yield child diff --git a/tests/test_black.py b/tests/test_black.py index 537ca80d432..56c20243020 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2388,6 +2388,27 @@ def test_empty_include(self) -> None: # Setting exclude explicitly to an empty string to block .gitignore usage. assert_collected_sources(src, expected, include="", exclude="") + def test_include_absolute_path(self) -> None: + path = DATA_DIR / "include_exclude_tests" + src = [path] + expected = [ + Path(path / "b/dont_exclude/a.pie"), + ] + assert_collected_sources( + src, expected, root=path, include=r"^/b/dont_exclude/a\.pie$", exclude="" + ) + + def test_exclude_absolute_path(self) -> None: + path = DATA_DIR / "include_exclude_tests" + src = [path] + expected = [ + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/.definitely_exclude/a.py"), + ] + assert_collected_sources( + src, expected, root=path, include=r"\.py$", exclude=r"^/b/exclude/a\.py$" + ) + def test_extend_exclude(self) -> None: path = DATA_DIR / "include_exclude_tests" src = [path] @@ -2401,7 +2422,6 @@ def test_extend_exclude(self) -> None: @pytest.mark.incompatible_with_mypyc def test_symlinks(self) -> None: - path = MagicMock() root = THIS_DIR.resolve() include = re.compile(black.DEFAULT_INCLUDES) exclude = re.compile(black.DEFAULT_EXCLUDES) @@ -2409,19 +2429,44 @@ def test_symlinks(self) -> None: gitignore = PathSpec.from_lines("gitwildmatch", []) regular = MagicMock() - outside_root_symlink = MagicMock() - ignored_symlink = MagicMock() - - path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink] - regular.absolute.return_value = root / "regular.py" regular.resolve.return_value = root / "regular.py" regular.is_dir.return_value = False + regular.is_file.return_value = True + outside_root_symlink = MagicMock() outside_root_symlink.absolute.return_value = root / "symlink.py" outside_root_symlink.resolve.return_value = Path("/nowhere") + outside_root_symlink.is_dir.return_value = False + outside_root_symlink.is_file.return_value = True + ignored_symlink = MagicMock() ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py" + ignored_symlink.is_dir.return_value = False + ignored_symlink.is_file.return_value = True + + # A symlink that has an excluded name, but points to an included name + symlink_excluded_name = MagicMock() + symlink_excluded_name.absolute.return_value = root / "excluded_name" + symlink_excluded_name.resolve.return_value = root / "included_name.py" + symlink_excluded_name.is_dir.return_value = False + symlink_excluded_name.is_file.return_value = True + + # A symlink that has an included name, but points to an excluded name + symlink_included_name = MagicMock() + symlink_included_name.absolute.return_value = root / "included_name.py" + symlink_included_name.resolve.return_value = root / "excluded_name" + symlink_included_name.is_dir.return_value = False + symlink_included_name.is_file.return_value = True + + path = MagicMock() + path.iterdir.return_value = [ + regular, + outside_root_symlink, + ignored_symlink, + symlink_excluded_name, + symlink_included_name, + ] files = list( black.gen_python_files( @@ -2437,7 +2482,7 @@ def test_symlinks(self) -> None: quiet=False, ) ) - assert files == [regular] + assert files == [regular, symlink_included_name] path.iterdir.assert_called_once() outside_root_symlink.resolve.assert_called_once() From caef19689b153f3a7baea1764a5adccae8bf1f1e Mon Sep 17 00:00:00 2001 From: Gabriel Perren Date: Fri, 27 Oct 2023 15:54:31 -0300 Subject: [PATCH 607/700] Update current_style.md (#3990) Fix small typo --- docs/the_black_code_style/current_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 431bae525f6..2a5e10162f2 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -178,7 +178,7 @@ If you use Flake8, you have a few options: extend-ignore = E203, E501, E704 ``` - The rationale for E950 is explained in + The rationale for B950 is explained in [Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings). 2. For a minimally compatible config: From c712d57ca9e30ba0db61c2fd7e4a2bf67f58bcc2 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:17:54 -0700 Subject: [PATCH 608/700] Add trailing comma test case for hugging parens (#3991) --- docs/the_black_code_style/future_style.md | 13 +++++++++++++ ...hug_parens_with_braces_and_square_brackets.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index f2534b0f0d0..c744902577d 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -139,6 +139,19 @@ foo([ ]) ``` +You can use a magic trailing comma to avoid this compacting behavior; by default, +_Black_ will not reformat the following code: + +```python +foo( + [ + 1, + 2, + 3, + ], +) +``` + ### Improved multiline string handling _Black_ is smarter when formatting multiline strings, especially in function arguments, diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 98ed342fcbc..6d10518133c 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -36,6 +36,14 @@ def foo_square_brackets(request): ] ) +func( + [ + 'a', + 'b', + 'c', + ], +) + func( # a [ # b "c", # c @@ -171,6 +179,14 @@ def foo_square_brackets(request): "c", ]) +func( + [ + "a", + "b", + "c", + ], +) + func([ # a # b "c", # c "d", # d From 53c4278a4c9b81baa86630ffda5f680f33968d1e Mon Sep 17 00:00:00 2001 From: Satyam Namdev <111422209+Spyrosigma@users.noreply.github.com> Date: Sat, 28 Oct 2023 01:57:19 +0530 Subject: [PATCH 609/700] Update CHANGES.md (#3988) Fixed a grammatical mistake --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 71f62d0e11f..84d9061135a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ ### Configuration -- Add support for single line format skip with other comments on the same line (#3959) +- Add support for single-line format skip with other comments on the same line (#3959) - Fix a bug in the matching of absolute path names in `--include` (#3976) From 7686989fc89aad5ea235a34977ebf8c81c26c4eb Mon Sep 17 00:00:00 2001 From: David Culley <6276049+davidculley@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:43:34 +0200 Subject: [PATCH 610/700] confine pre-commit to stages (#3940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://pre-commit.com/#confining-hooks-to-run-at-certain-stages > If you are authoring a tool, it is usually a good idea to provide an appropriate `stages` property. For example a reasonable setting for a linter or code formatter would be `stages: [pre-commit, pre-merge-commit, pre-push, manual]`. Co-authored-by: Jelle Zijlstra --- .pre-commit-hooks.yaml | 2 ++ CHANGES.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index a1ff41fded8..54a03efe7a1 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,6 +4,7 @@ name: black description: "Black: The uncompromising Python code formatter" entry: black + stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true @@ -13,6 +14,7 @@ description: "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" entry: black + stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true diff --git a/CHANGES.md b/CHANGES.md index 84d9061135a..60231468bdf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,9 @@ +- Black's pre-commit integration will now run only on git hooks appropriate for a code + formatter (#3940) + ### Documentation in CHANGES.md to delete ... - Update ci to run out of scripts dir too - Update test_tuple_calver --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- .github/workflows/release_tests.yml | 56 ++++++ docs/contributing/release_process.md | 70 ++------ scripts/release.py | 243 +++++++++++++++++++++++++++ scripts/release_tests.py | 69 ++++++++ 4 files changed, 383 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/release_tests.yml create mode 100755 scripts/release.py create mode 100644 scripts/release_tests.py diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml new file mode 100644 index 00000000000..74729445052 --- /dev/null +++ b/.github/workflows/release_tests.yml @@ -0,0 +1,56 @@ +name: Release tool CI + +on: + push: + paths: + - .github/workflows/release_tests.yml + - release.py + - release_tests.py + pull_request: + paths: + - .github/workflows/release_tests.yml + - release.py + - release_tests.py + +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 + + name: Running python ${{ matrix.python-version }} on ${{matrix.os}} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.12"] + os: [macOS-latest, ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + with: + # Give us all history, branches and tags + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Print Python Version + run: python --version --version && which python + + - name: Print Git Version + run: git --version && which git + + - name: Update pip, setuptools + wheels + run: | + python -m pip install --upgrade pip setuptools wheel + + - name: Run unit tests via coverage + print report + run: | + python -m pip install coverage + coverage run scripts/release_tests.py + coverage report --show-missing diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index 02865d6f4bd..c66ffae8ace 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -32,21 +32,29 @@ The 10,000 foot view of the release process is that you prepare a release PR and publish a [GitHub Release]. This triggers [release automation](#release-workflows) that builds all release artifacts and publishes them to the various platforms we publish to. +We now have a `scripts/release.py` script to help with cutting the release PRs. + +- `python3 scripts/release.py --help` is your friend. + - `release.py` has only been tested in Python 3.12 (so get with the times :D) + To cut a release: 1. Determine the release's version number - **_Black_ follows the [CalVer] versioning standard using the `YY.M.N` format** - So unless there already has been a release during this month, `N` should be `0` - Example: the first release in January, 2022 → `22.1.0` + - `release.py` will calculate this and log to stderr for you copy paste pleasure 1. File a PR editing `CHANGES.md` and the docs to version the latest changes + - Run `python3 scripts/release.py [--debug]` to generate most changes + - Sub headings in the template, if they have no bullet points need manual removal + _PR welcome to improve :D_ +1. If `release.py` fail manually edit; otherwise, yay, skip this step! 1. Replace the `## Unreleased` header with the version number 1. Remove any empty sections for the current release 1. (_optional_) Read through and copy-edit the changelog (eg. by moving entries, fixing typos, or rephrasing entries) 1. Double-check that no changelog entries since the last release were put in the wrong section (e.g., run `git diff CHANGES.md`) - 1. Add a new empty template for the next release above - ([template below](#changelog-template)) 1. Update references to the latest version in {doc}`/integrations/source_version_control` and {doc}`/usage_and_configuration/the_basics` @@ -63,6 +71,11 @@ To cut a release: description box 1. Publish the GitHub Release, triggering [release automation](#release-workflows) that will handle the rest +1. Once CI is done add + commit (git push - No review) a new empty template for the next + release to CHANGES.md _(Template is able to be copy pasted from release.py should we + fail)_ + 1. `python3 scripts/release.py --add-changes-template|-a [--debug]` + 1. Should that fail, please return to copy + paste 1. At this point, you're basically done. It's good practice to go and [watch and verify that all the release workflows pass][black-actions], although you will receive a GitHub notification should something fail. @@ -81,59 +94,6 @@ release is probably unnecessary. In the end, use your best judgement and ask other maintainers for their thoughts. ``` -### Changelog template - -Use the following template for a clean changelog after the release: - -``` -## Unreleased - -### Highlights - - - -### Stable style - - - -### Preview style - - - -### Configuration - - - -### Packaging - - - -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - -### Integrations - - - -### Documentation - - -``` - ## Release workflows All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore diff --git a/scripts/release.py b/scripts/release.py new file mode 100755 index 00000000000..d588429c2d3 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +""" +Tool to help automate changes needed in commits during and after releases +""" + +import argparse +import logging +import sys +from datetime import datetime +from pathlib import Path +from subprocess import PIPE, run +from typing import List + +LOG = logging.getLogger(__name__) +NEW_VERSION_CHANGELOG_TEMPLATE = """\ +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + +""" + + +class NoGitTagsError(Exception): ... # noqa: E701,E761 + + +# TODO: Do better with alpha + beta releases +# Maybe we vendor packaging library +def get_git_tags(versions_only: bool = True) -> List[str]: + """Pull out all tags or calvers only""" + cp = run(["git", "tag"], stdout=PIPE, stderr=PIPE, check=True, encoding="utf8") + if not cp.stdout: + LOG.error(f"Returned no git tags stdout: {cp.stderr}") + raise NoGitTagsError + git_tags = cp.stdout.splitlines() + if versions_only: + return [t for t in git_tags if t[0].isdigit()] + return git_tags + + +# TODO: Support sorting alhpa/beta releases correctly +def tuple_calver(calver: str) -> tuple[int, ...]: # mypy can't notice maxsplit below + """Convert a calver string into a tuple of ints for sorting""" + try: + return tuple(map(int, calver.split(".", maxsplit=2))) + except ValueError: + return (0, 0, 0) + + +class SourceFiles: + def __init__(self, black_repo_dir: Path): + # File path fun all pathlib to be platform agnostic + self.black_repo_path = black_repo_dir + self.changes_path = self.black_repo_path / "CHANGES.md" + self.docs_path = self.black_repo_path / "docs" + self.version_doc_paths = ( + self.docs_path / "integrations" / "source_version_control.md", + self.docs_path / "usage_and_configuration" / "the_basics.md", + ) + self.current_version = self.get_current_version() + self.next_version = self.get_next_version() + + def __str__(self) -> str: + return f"""\ +> SourceFiles ENV: + Repo path: {self.black_repo_path} + CHANGES.md path: {self.changes_path} + docs path: {self.docs_path} + Current version: {self.current_version} + Next version: {self.next_version} +""" + + def add_template_to_changes(self) -> int: + """Add the template to CHANGES.md if it does not exist""" + LOG.info(f"Adding template to {self.changes_path}") + + with self.changes_path.open("r") as cfp: + changes_string = cfp.read() + + if "## Unreleased" in changes_string: + LOG.error(f"{self.changes_path} already has unreleased template") + return 1 + + templated_changes_string = changes_string.replace( + "# Change Log\n", + f"# Change Log\n\n{NEW_VERSION_CHANGELOG_TEMPLATE}", + ) + + with self.changes_path.open("w") as cfp: + cfp.write(templated_changes_string) + + LOG.info(f"Added template to {self.changes_path}") + return 0 + + def cleanup_changes_template_for_release(self) -> None: + LOG.info(f"Cleaning up {self.changes_path}") + + with self.changes_path.open("r") as cfp: + changes_string = cfp.read() + + # Change Unreleased to next version + versioned_changes = changes_string.replace( + "## Unreleased", f"## {self.next_version}" + ) + + # Remove all comments (subheadings are harder - Human required still) + no_comments_changes = [] + for line in versioned_changes.splitlines(): + if line.startswith(""): + continue + no_comments_changes.append(line) + + with self.changes_path.open("w") as cfp: + cfp.write("\n".join(no_comments_changes) + "\n") + + LOG.debug(f"Finished Cleaning up {self.changes_path}") + + def get_current_version(self) -> str: + """Get the latest git (version) tag as latest version""" + return sorted(get_git_tags(), key=lambda k: tuple_calver(k))[-1] + + def get_next_version(self) -> str: + """Workout the year and month + version number we need to move to""" + base_calver = datetime.today().strftime("%y.%m") + calver_parts = base_calver.split(".") + base_calver = f"{calver_parts[0]}.{int(calver_parts[1])}" # Remove leading 0 + git_tags = get_git_tags() + same_month_releases = [t for t in git_tags if t.startswith(base_calver)] + if len(same_month_releases) < 1: + return f"{base_calver}.0" + same_month_version = same_month_releases[-1].split(".", 2)[-1] + return f"{base_calver}.{int(same_month_version) + 1}" + + def update_repo_for_release(self) -> int: + """Update CHANGES.md + doc files ready for release""" + self.cleanup_changes_template_for_release() + self.update_version_in_docs() + return 0 # return 0 if no exceptions hit + + def update_version_in_docs(self) -> None: + for doc_path in self.version_doc_paths: + LOG.info(f"Updating black version to {self.next_version} in {doc_path}") + + with doc_path.open("r") as dfp: + doc_string = dfp.read() + + next_version_doc = doc_string.replace( + self.current_version, self.next_version + ) + + with doc_path.open("w") as dfp: + dfp.write(next_version_doc) + + LOG.debug( + f"Finished updating black version to {self.next_version} in {doc_path}" + ) + + +def _handle_debug(debug: bool) -> None: + """Turn on debugging if asked otherwise INFO default""" + log_level = logging.DEBUG if debug else logging.INFO + logging.basicConfig( + format="[%(asctime)s] %(levelname)s: %(message)s (%(filename)s:%(lineno)d)", + level=log_level, + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "-a", + "--add-changes-template", + action="store_true", + help="Add the Unreleased template to CHANGES.md", + ) + parser.add_argument( + "-d", "--debug", action="store_true", help="Verbose debug output" + ) + args = parser.parse_args() + _handle_debug(args.debug) + return args + + +def main() -> int: + args = parse_args() + + # Need parent.parent cause script is in scripts/ directory + sf = SourceFiles(Path(__file__).parent.parent) + + if args.add_changes_template: + return sf.add_template_to_changes() + + LOG.info(f"Current version detected to be {sf.current_version}") + LOG.info(f"Next version will be {sf.next_version}") + return sf.update_repo_for_release() + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/scripts/release_tests.py b/scripts/release_tests.py new file mode 100644 index 00000000000..bd72cb4b48a --- /dev/null +++ b/scripts/release_tests.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import unittest +from pathlib import Path +from shutil import rmtree +from tempfile import TemporaryDirectory +from typing import Any +from unittest.mock import Mock, patch + +from release import SourceFiles, tuple_calver # type: ignore + + +class FakeDateTime: + """Used to mock the date to test generating next calver function""" + + def today(*args: Any, **kwargs: Any) -> "FakeDateTime": # noqa + return FakeDateTime() + + # Add leading 0 on purpose to ensure we remove it + def strftime(*args: Any, **kwargs: Any) -> str: # noqa + return "69.01" + + +class TestRelease(unittest.TestCase): + def setUp(self) -> None: + # We only test on >= 3.12 + self.tempdir = TemporaryDirectory(delete=False) # type: ignore + self.tempdir_path = Path(self.tempdir.name) + self.sf = SourceFiles(self.tempdir_path) + + def tearDown(self) -> None: + rmtree(self.tempdir.name) + return super().tearDown() + + @patch("release.get_git_tags") + def test_get_current_version(self, mocked_git_tags: Mock) -> None: + mocked_git_tags.return_value = ["1.1.0", "69.1.0", "69.1.1", "2.2.0"] + self.assertEqual("69.1.1", self.sf.get_current_version()) + + @patch("release.get_git_tags") + @patch("release.datetime", FakeDateTime) + def test_get_next_version(self, mocked_git_tags: Mock) -> None: + # test we handle no args + mocked_git_tags.return_value = [] + self.assertEqual( + "69.1.0", + self.sf.get_next_version(), + "Unable to get correct next version with no git tags", + ) + + # test we handle + mocked_git_tags.return_value = ["1.1.0", "69.1.0", "69.1.1", "2.2.0"] + self.assertEqual( + "69.1.2", + self.sf.get_next_version(), + "Unable to get correct version with 2 previous versions released this" + " month", + ) + + def test_tuple_calver(self) -> None: + first_month_release = tuple_calver("69.1.0") + second_month_release = tuple_calver("69.1.1") + self.assertEqual((69, 1, 0), first_month_release) + self.assertEqual((0, 0, 0), tuple_calver("69.1.1a0")) # Hack for alphas/betas + self.assertTrue(first_month_release < second_month_release) + + +if __name__ == "__main__": + unittest.main() From ddfecf06c13dd86205c851e340124e325ed82c5c Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Mon, 30 Oct 2023 17:35:26 +0200 Subject: [PATCH 615/700] Hug parens also with multiline unpacking (#3992) --- CHANGES.md | 2 ++ docs/the_black_code_style/future_style.md | 20 +++++++++++ src/black/cache.py | 6 ++-- src/black/linegen.py | 7 ++-- ..._parens_with_braces_and_square_brackets.py | 36 +++++++++++++++++++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 60231468bdf..dd5f52cf706 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ - Multiline dictionaries and lists that are the sole argument to a function are now indented less (#3964) +- Multiline list and dict unpacking as the sole argument to a function is now also + indented less (#3992) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index c744902577d..944ffad033e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -139,6 +139,26 @@ foo([ ]) ``` +This also applies to list and dictionary unpacking: + +```python +foo( + *[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) +``` + +will become: + +```python +foo(*[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator +]) +``` + You can use a magic trailing comma to avoid this compacting behavior; by default, _Black_ will not reformat the following code: diff --git a/src/black/cache.py b/src/black/cache.py index 6baa096baca..6a332304981 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -124,9 +124,9 @@ def filtered_cached(self, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path] def write(self, sources: Iterable[Path]) -> None: """Update the cache file data and write a new cache file.""" - self.file_data.update( - **{str(src.resolve()): Cache.get_file_data(src) for src in sources} - ) + self.file_data.update(**{ + str(src.resolve()): Cache.get_file_data(src) for src in sources + }) try: CACHE_DIR.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( diff --git a/src/black/linegen.py b/src/black/linegen.py index 5f5a69152d5..43bc08efbbd 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -817,16 +817,17 @@ def _first_right_hand_split( head_leaves.reverse() if Preview.hug_parens_with_braces_and_square_brackets in line.mode: + is_unpacking = 1 if body_leaves[0].type in [token.STAR, token.DOUBLESTAR] else 0 if ( tail_leaves[0].type == token.RPAR and tail_leaves[0].value and tail_leaves[0].opening_bracket is head_leaves[-1] and body_leaves[-1].type in [token.RBRACE, token.RSQB] - and body_leaves[-1].opening_bracket is body_leaves[0] + and body_leaves[-1].opening_bracket is body_leaves[is_unpacking] ): - head_leaves = head_leaves + body_leaves[:1] + head_leaves = head_leaves + body_leaves[: 1 + is_unpacking] tail_leaves = body_leaves[-1:] + tail_leaves - body_leaves = body_leaves[1:-1] + body_leaves = body_leaves[1 + is_unpacking : -1] head = bracket_split_build_line( head_leaves, line, opening_bracket, component=_BracketSplitComponent.head diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 6d10518133c..51fe516add5 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -137,6 +137,21 @@ def foo_square_brackets(request): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) +foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) + +foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) + +foo( + **{ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": 1, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": 2, + "ccccccccccccccccccccccccccccccccc": 3, + **other, + } +) + +foo(**{x: y for x, y in enumerate(["long long long long line","long long long long line"])}) + # output def foo_brackets(request): return JsonResponse({ @@ -287,3 +302,24 @@ def foo_square_brackets(request): baaaaaaaaaaaaar( [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) + +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) + +foo(*[ + str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) +]) + +foo(**{ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": 1, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": 2, + "ccccccccccccccccccccccccccccccccc": 3, + **other, +}) + +foo(**{ + x: y for x, y in enumerate(["long long long long line", "long long long long line"]) +}) From e50110353ab81b539aaee686453c18c707b5f045 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 31 Oct 2023 17:27:11 +0200 Subject: [PATCH 616/700] Produce equivalent code for docstrings containing backslash followed by whitespace(s) before newline (#4008) Fixes #3727 --- CHANGES.md | 3 ++- src/black/linegen.py | 3 ++- tests/data/cases/docstring.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dd5f52cf706..e910fbed162 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,8 @@ ### Stable style - +- Fix a crash when whitespace(s) followed a backslash before newline in a docstring + (#4008) ### Preview style diff --git a/src/black/linegen.py b/src/black/linegen.py index 43bc08efbbd..121c6e314fe 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -2,6 +2,7 @@ Generating lines of code. """ +import re import sys from dataclasses import replace from enum import Enum, auto @@ -420,7 +421,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) - if is_docstring(leaf) and "\\\n" not in leaf.value: + if is_docstring(leaf) and not re.search(r"\\\s*\n", leaf.value): # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. if self.mode.string_normalization: diff --git a/tests/data/cases/docstring.py b/tests/data/cases/docstring.py index c31d6a68783..e983c5bd438 100644 --- a/tests/data/cases/docstring.py +++ b/tests/data/cases/docstring.py @@ -221,6 +221,12 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ''' +def foo(): + """ + Docstring with a backslash followed by a space\ + and then another line + """ + # output class MyClass: @@ -442,3 +448,10 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): """ + + +def foo(): + """ + Docstring with a backslash followed by a space\ + and then another line + """ From 5758da6e3cda4ec037c5dbb7867373cf694edd03 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 31 Oct 2023 17:11:28 -0700 Subject: [PATCH 617/700] Fix bytes strings being treated as docstrings (#4003) Fixes #4002 --- CHANGES.md | 4 +-- src/black/nodes.py | 9 ++++++- tests/data/cases/bytes_docstring.py | 34 +++++++++++++++++++++++++ tests/data/{ => cases}/raw_docstring.py | 0 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/data/cases/bytes_docstring.py rename tests/data/{ => cases}/raw_docstring.py (100%) diff --git a/CHANGES.md b/CHANGES.md index e910fbed162..f365f1c239b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,8 +8,8 @@ ### Stable style -- Fix a crash when whitespace(s) followed a backslash before newline in a docstring - (#4008) +- Fix crash on formatting bytes strings that look like docstrings (#4003) +- Fix crash when whitespace followed a backslash before newline in a docstring (#4008) ### Preview style diff --git a/src/black/nodes.py b/src/black/nodes.py index b2e96cb9edf..5f6b280c035 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -14,7 +14,7 @@ from black.cache import CACHE_DIR from black.mode import Mode, Preview -from black.strings import has_triple_quotes +from black.strings import get_string_prefix, has_triple_quotes from blib2to3 import pygram from blib2to3.pgen2 import token from blib2to3.pytree import NL, Leaf, Node, type_repr @@ -525,6 +525,13 @@ def is_arith_like(node: LN) -> bool: def is_docstring(leaf: Leaf) -> bool: + if leaf.type != token.STRING: + return False + + prefix = get_string_prefix(leaf.value) + if "b" in prefix or "B" in prefix: + return False + if prev_siblings_are( leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] ): diff --git a/tests/data/cases/bytes_docstring.py b/tests/data/cases/bytes_docstring.py new file mode 100644 index 00000000000..2326e95293a --- /dev/null +++ b/tests/data/cases/bytes_docstring.py @@ -0,0 +1,34 @@ +def bitey(): + b" not a docstring" + +def bitey2(): + b' also not a docstring' + +def triple_quoted_bytes(): + b""" not a docstring""" + +def triple_quoted_bytes2(): + b''' also not a docstring''' + +def capitalized_bytes(): + B" NOT A DOCSTRING" + +# output +def bitey(): + b" not a docstring" + + +def bitey2(): + b" also not a docstring" + + +def triple_quoted_bytes(): + b""" not a docstring""" + + +def triple_quoted_bytes2(): + b""" also not a docstring""" + + +def capitalized_bytes(): + b" NOT A DOCSTRING" \ No newline at end of file diff --git a/tests/data/raw_docstring.py b/tests/data/cases/raw_docstring.py similarity index 100% rename from tests/data/raw_docstring.py rename to tests/data/cases/raw_docstring.py From e2f2bd076fbc19d4adb90b70b5a7be32b08d5dbe Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 1 Nov 2023 06:20:14 -0700 Subject: [PATCH 618/700] Minor refactoring in get_sources and gen_python_files (#4013) --- src/black/__init__.py | 39 ++++++++++++++++++--------------------- src/black/files.py | 7 ++----- tests/test_black.py | 5 +++-- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 7cf93b89e42..c11a66b7bc8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -50,6 +50,7 @@ get_gitignore, normalize_path_maybe_ignore, parse_pyproject_toml, + path_is_excluded, wrap_stream_for_windows, ) from black.handle_ipynb_magics import ( @@ -632,15 +633,15 @@ def get_sources( for s in src: if s == "-" and stdin_filename: - p = Path(stdin_filename) + path = Path(stdin_filename) is_stdin = True else: - p = Path(s) + path = Path(s) is_stdin = False - if is_stdin or p.is_file(): + if is_stdin or path.is_file(): normalized_path: Optional[str] = normalize_path_maybe_ignore( - p, root, report + path, root, report ) if normalized_path is None: if verbose: @@ -651,38 +652,34 @@ def get_sources( normalized_path = "/" + normalized_path # Hard-exclude any files that matches the `--force-exclude` regex. - if force_exclude: - force_exclude_match = force_exclude.search(normalized_path) - else: - force_exclude_match = None - if force_exclude_match and force_exclude_match.group(0): - report.path_ignored(p, "matches the --force-exclude regular expression") + if path_is_excluded(normalized_path, force_exclude): + report.path_ignored( + path, "matches the --force-exclude regular expression" + ) continue if is_stdin: - p = Path(f"{STDIN_PLACEHOLDER}{str(p)}") + path = Path(f"{STDIN_PLACEHOLDER}{str(path)}") - if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed( + if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed( warn=verbose or not quiet ): continue - sources.add(p) - elif p.is_dir(): - p_relative = normalize_path_maybe_ignore(p, root, report) - assert p_relative is not None - p = root / p_relative + sources.add(path) + elif path.is_dir(): + path = root / (path.resolve().relative_to(root)) if verbose: - out(f'Found input source directory: "{p}"', fg="blue") + out(f'Found input source directory: "{path}"', fg="blue") if using_default_exclude: gitignore = { root: root_gitignore, - p: get_gitignore(p), + path: get_gitignore(path), } sources.update( gen_python_files( - p.iterdir(), + path.iterdir(), root, include, exclude, @@ -697,7 +694,7 @@ def get_sources( elif s == "-": if verbose: out("Found input source stdin", fg="blue") - sources.add(p) + sources.add(path) else: err(f"invalid path: {s}") diff --git a/src/black/files.py b/src/black/files.py index 1eed7eda828..858303ca1a3 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -280,7 +280,6 @@ def _path_is_ignored( root_relative_path: str, root: Path, gitignore_dict: Dict[Path, PathSpec], - report: Report, ) -> bool: path = root / root_relative_path # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must @@ -291,9 +290,6 @@ def _path_is_ignored( except ValueError: break if pattern.match_file(relative_path): - report.path_ignored( - path.relative_to(root), "matches a .gitignore file content" - ) return True return False @@ -334,8 +330,9 @@ def gen_python_files( # First ignore files matching .gitignore, if passed if gitignore_dict and _path_is_ignored( - root_relative_path, root, gitignore_dict, report + root_relative_path, root, gitignore_dict ): + report.path_ignored(child, "matches a .gitignore file content") continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. diff --git a/tests/test_black.py b/tests/test_black.py index 56c20243020..c7196098e14 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -504,7 +504,7 @@ def _mocked_calls() -> bool: return _mocked_calls with patch("pathlib.Path.iterdir", return_value=target_contents), patch( - "pathlib.Path.cwd", return_value=working_directory + "pathlib.Path.resolve", return_value=target_abspath ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])): # Note that the root folder (project_root) isn't the folder # named "root" (aka working_directory) @@ -526,7 +526,8 @@ def _mocked_calls() -> bool: for _, mock_args, _ in report.path_ignored.mock_calls ), "A symbolic link was reported." report.path_ignored.assert_called_once_with( - Path("root", "child", "b.py"), "matches a .gitignore file content" + Path(working_directory, "child", "b.py"), + "matches a .gitignore file content", ) def test_report_verbose(self) -> None: From c54c213d6a3132986feede0cf0525f5bae5b43d6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Nov 2023 20:42:11 -0700 Subject: [PATCH 619/700] Fix crash on await (a ** b) (#3994) --- CHANGES.md | 2 ++ src/black/linegen.py | 22 ++++++++++------------ tests/data/cases/remove_await_parens.py | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f365f1c239b..5ce37943693 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,8 @@ - Fix crash on formatting bytes strings that look like docstrings (#4003) - Fix crash when whitespace followed a backslash before newline in a docstring (#4008) +- Fix crash on formatting code like `await (a ** b)` (#3994) + ### Preview style - Multiline dictionaries and lists that are the sole argument to a function are now diff --git a/src/black/linegen.py b/src/black/linegen.py index 121c6e314fe..b13b95d9b31 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1352,18 +1352,16 @@ def remove_await_parens(node: Node) -> None: opening_bracket = cast(Leaf, node.children[1].children[0]) closing_bracket = cast(Leaf, node.children[1].children[-1]) bracket_contents = node.children[1].children[1] - if isinstance(bracket_contents, Node): - if bracket_contents.type != syms.power: - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - elif ( - bracket_contents.type == syms.power - and bracket_contents.children[0].type == token.AWAIT - ): - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - # If we are in a nested await then recurse down. - remove_await_parens(bracket_contents) + if isinstance(bracket_contents, Node) and ( + bracket_contents.type != syms.power + or bracket_contents.children[0].type == token.AWAIT + or any( + isinstance(child, Leaf) and child.type == token.DOUBLESTAR + for child in bracket_contents.children + ) + ): + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) def _maybe_wrap_cms_in_parens( diff --git a/tests/data/cases/remove_await_parens.py b/tests/data/cases/remove_await_parens.py index 8c7223d2f39..073150c5f08 100644 --- a/tests/data/cases/remove_await_parens.py +++ b/tests/data/cases/remove_await_parens.py @@ -80,6 +80,15 @@ async def main(): async def main(): await (yield) +async def main(): + await (a ** b) + await (a[b] ** c) + await (a ** b[c]) + await ((a + b) ** (c + d)) + await (a + b) + await (a[b]) + await (a[b ** c]) + # output import asyncio @@ -174,3 +183,13 @@ async def main(): async def main(): await (yield) + + +async def main(): + await (a**b) + await (a[b] ** c) + await (a ** b[c]) + await ((a + b) ** (c + d)) + await (a + b) + await a[b] + await a[b**c] From 448324637d12514b540efb33b4df7bf8af10c6d5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 4 Nov 2023 22:49:12 +0200 Subject: [PATCH 620/700] Enable branch coverage (#4022) When trying to understand the code logic, and looking at coverage reports, branch coverage is very helpful. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8c55076e4c9..f3689bfb746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,7 @@ omit = [ ] [tool.coverage.run] relative_files = true +branch = true [tool.mypy] # Specify the target platform details in config, so your developers are From 9e3daa1107a66f311a8367395a33ed5fc5d5e73d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 5 Nov 2023 18:29:37 -0800 Subject: [PATCH 621/700] Fix arm wheels on macOS (#4017) --- .github/workflows/pypi_upload.yml | 7 ++++--- CHANGES.md | 2 ++ pyproject.toml | 13 ++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index a57013d67c1..07273f09508 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -68,9 +68,10 @@ jobs: - name: generate matrix (PR) if: github.event_name == 'pull_request' run: | - cibuildwheel --print-build-identifiers --platform linux \ - | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' \ - | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix + { + cibuildwheel --print-build-identifiers --platform linux \ + | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' + } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix env: CIBW_BUILD: "cp38-* cp311-*" CIBW_ARCHS_LINUX: x86_64 diff --git a/CHANGES.md b/CHANGES.md index 5ce37943693..97084a2bfc1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,8 @@ +- Fix mypyc builds on arm64 on macOS (#4017) + ### Output diff --git a/pyproject.toml b/pyproject.toml index f3689bfb746..c0302d2302a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,8 @@ exclude = ["/profiling"] [tool.hatch.build.targets.wheel] only-include = ["src"] sources = ["src"] +# Note that we change the behaviour of this flag below +macos-max-compat = true [tool.hatch.build.targets.wheel.hooks.mypyc] enable-by-default = false @@ -175,9 +177,18 @@ before-build = [ HATCH_BUILD_HOOKS_ENABLE = "1" MYPYC_OPT_LEVEL = "3" MYPYC_DEBUG_LEVEL = "0" +AIOHTTP_NO_EXTENSIONS = "1" + # Black needs Clang to compile successfully on Linux. CC = "clang" -AIOHTTP_NO_EXTENSIONS = "1" + +[tool.cibuildwheel.macos] +build-frontend = { name = "build", args = ["--no-isolation"] } +# Unfortunately, hatch doesn't respect MACOSX_DEPLOYMENT_TARGET +before-build = [ + "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.5.1' 'click==8.1.3'", + """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, +] [tool.isort] atomic = true From e808e61db8c7a8f9c7fd4b2fff2281141f6b2517 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Mon, 6 Nov 2023 14:30:04 -0800 Subject: [PATCH 622/700] Preview: Keep requiring two empty lines between module-level docstring and first function or class definition (#4028) Fixes #4027. --- CHANGES.md | 2 ++ src/black/lines.py | 1 + .../data/cases/module_docstring_followed_by_class.py | 11 +++++++++++ .../cases/module_docstring_followed_by_function.py | 11 +++++++++++ 4 files changed, 25 insertions(+) create mode 100644 tests/data/cases/module_docstring_followed_by_class.py create mode 100644 tests/data/cases/module_docstring_followed_by_function.py diff --git a/CHANGES.md b/CHANGES.md index 97084a2bfc1..a68f87bfc12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,8 @@ indented less (#3964) - Multiline list and dict unpacking as the sole argument to a function is now also indented less (#3992) +- Keep requiring two empty lines between module-level docstring and first function or + class definition. (#4028) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index a73c429e3d9..23c1a93d3d4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -578,6 +578,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 and self.previous_block.original_line.is_triple_quoted_string + and not (current_line.is_class or current_line.is_def) ): before = 1 diff --git a/tests/data/cases/module_docstring_followed_by_class.py b/tests/data/cases/module_docstring_followed_by_class.py new file mode 100644 index 00000000000..6fdbfc8c240 --- /dev/null +++ b/tests/data/cases/module_docstring_followed_by_class.py @@ -0,0 +1,11 @@ +# flags: --preview +"""Two blank lines between module docstring and a class.""" +class MyClass: + pass + +# output +"""Two blank lines between module docstring and a class.""" + + +class MyClass: + pass diff --git a/tests/data/cases/module_docstring_followed_by_function.py b/tests/data/cases/module_docstring_followed_by_function.py new file mode 100644 index 00000000000..5913a59e1fe --- /dev/null +++ b/tests/data/cases/module_docstring_followed_by_function.py @@ -0,0 +1,11 @@ +# flags: --preview +"""Two blank lines between module docstring and a function def.""" +def function(): + pass + +# output +"""Two blank lines between module docstring and a function def.""" + + +def function(): + pass From ecbd9e8cf71f13068c7e6803a534e00363114c91 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:58:43 -0800 Subject: [PATCH 623/700] Fix crash with f-string docstrings (#4019) Python does not consider f-strings to be docstrings, so we probably shouldn't be formatting them as such Fixes #4018 Co-authored-by: Alex Waygood --- CHANGES.md | 3 +++ src/black/nodes.py | 2 +- tests/data/cases/docstring_preview.py | 3 ++- tests/data/cases/f_docstring.py | 20 +++++++++++++++++++ ...view_docstring_no_string_normalization.py} | 0 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/data/cases/f_docstring.py rename tests/data/cases/{docstring_preview_no_string_normalization.py => preview_docstring_no_string_normalization.py} (100%) diff --git a/CHANGES.md b/CHANGES.md index a68f87bfc12..b1fe25ef625 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,9 @@ - Fix crash on formatting code like `await (a ** b)` (#3994) +- No longer treat leading f-strings as docstrings. This matches Python's behaviour and + fixes a crash (#4019) + ### Preview style - Multiline dictionaries and lists that are the sole argument to a function are now diff --git a/src/black/nodes.py b/src/black/nodes.py index 5f6b280c035..fff8e05a118 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -529,7 +529,7 @@ def is_docstring(leaf: Leaf) -> bool: return False prefix = get_string_prefix(leaf.value) - if "b" in prefix or "B" in prefix: + if set(prefix).intersection("bBfF"): return False if prev_siblings_are( diff --git a/tests/data/cases/docstring_preview.py b/tests/data/cases/docstring_preview.py index ff4819acb67..a3c656be2f8 100644 --- a/tests/data/cases/docstring_preview.py +++ b/tests/data/cases/docstring_preview.py @@ -58,7 +58,8 @@ def docstring_almost_at_line_limit(): def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................""" + f"""long docstring................................................................ + """ def mulitline_docstring_almost_at_line_limit(): diff --git a/tests/data/cases/f_docstring.py b/tests/data/cases/f_docstring.py new file mode 100644 index 00000000000..667f550b353 --- /dev/null +++ b/tests/data/cases/f_docstring.py @@ -0,0 +1,20 @@ +def foo(e): + f""" {'.'.join(e)}""" + +def bar(e): + f"{'.'.join(e)}" + +def baz(e): + F""" {'.'.join(e)}""" + +# output +def foo(e): + f""" {'.'.join(e)}""" + + +def bar(e): + f"{'.'.join(e)}" + + +def baz(e): + f""" {'.'.join(e)}""" diff --git a/tests/data/cases/docstring_preview_no_string_normalization.py b/tests/data/cases/preview_docstring_no_string_normalization.py similarity index 100% rename from tests/data/cases/docstring_preview_no_string_normalization.py rename to tests/data/cases/preview_docstring_no_string_normalization.py From 46be1f8e54ac9a7d67723c0fa28c7bec13a0a2bf Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Mon, 6 Nov 2023 18:05:25 -0800 Subject: [PATCH 624/700] Support formatting specified lines (#4020) --- CHANGES.md | 3 + docs/usage_and_configuration/the_basics.md | 17 + src/black/__init__.py | 130 ++++- src/black/nodes.py | 28 + src/black/ranges.py | 496 ++++++++++++++++++ tests/data/cases/line_ranges_basic.py | 107 ++++ tests/data/cases/line_ranges_fmt_off.py | 49 ++ .../cases/line_ranges_fmt_off_decorator.py | 27 + .../data/cases/line_ranges_fmt_off_overlap.py | 37 ++ tests/data/cases/line_ranges_imports.py | 9 + tests/data/cases/line_ranges_indentation.py | 27 + tests/data/cases/line_ranges_two_passes.py | 27 + tests/data/cases/line_ranges_unwrapping.py | 25 + tests/data/invalid_line_ranges.toml | 2 + tests/data/line_ranges_formatted/basic.py | 50 ++ .../line_ranges_formatted/pattern_matching.py | 25 + tests/test_black.py | 87 ++- tests/test_format.py | 26 +- tests/test_ranges.py | 185 +++++++ tests/util.py | 29 +- 20 files changed, 1358 insertions(+), 28 deletions(-) create mode 100644 src/black/ranges.py create mode 100644 tests/data/cases/line_ranges_basic.py create mode 100644 tests/data/cases/line_ranges_fmt_off.py create mode 100644 tests/data/cases/line_ranges_fmt_off_decorator.py create mode 100644 tests/data/cases/line_ranges_fmt_off_overlap.py create mode 100644 tests/data/cases/line_ranges_imports.py create mode 100644 tests/data/cases/line_ranges_indentation.py create mode 100644 tests/data/cases/line_ranges_two_passes.py create mode 100644 tests/data/cases/line_ranges_unwrapping.py create mode 100644 tests/data/invalid_line_ranges.toml create mode 100644 tests/data/line_ranges_formatted/basic.py create mode 100644 tests/data/line_ranges_formatted/pattern_matching.py create mode 100644 tests/test_ranges.py diff --git a/CHANGES.md b/CHANGES.md index b1fe25ef625..780a00247ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ +- Support formatting ranges of lines with the new `--line-ranges` command-line option + (#4020). + ### Stable style - Fix crash on formatting bytes strings that look like docstrings (#4003) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index f25dbb13d4d..dbd8c7ba434 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -175,6 +175,23 @@ All done! ✨ 🍰 ✨ 1 file would be reformatted. ``` +### `--line-ranges` + +When specified, _Black_ will try its best to only format these lines. + +This option can be specified multiple times, and a union of the lines will be formatted. +Each range must be specified as two integers connected by a `-`: `-`. The +`` and `` integer indices are 1-based and inclusive on both ends. + +_Black_ may still format lines outside of the ranges for multi-line statements. +Formatting more than one file or any ipynb files with this option is not supported. This +option cannot be specified in the `pyproject.toml` config. + +Example: `black --line-ranges=1-10 --line-ranges=21-30 test.py` will format lines from +`1` to `10` and `21` to `30`. + +This option is mainly for editor integrations, such as "Format Selection". + #### `--color` / `--no-color` Show (or do not show) colored diff. Only applies when `--diff` is given. diff --git a/src/black/__init__.py b/src/black/__init__.py index c11a66b7bc8..5aca3316df0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import ( Any, + Collection, Dict, Generator, Iterator, @@ -77,6 +78,7 @@ from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out from black.parsing import InvalidInput # noqa F401 from black.parsing import lib2to3_parse, parse_ast, stringify_ast +from black.ranges import adjusted_lines, convert_unchanged_lines, parse_line_ranges from black.report import Changed, NothingChanged, Report from black.trans import iter_fexpr_spans from blib2to3.pgen2 import token @@ -163,6 +165,12 @@ def read_pyproject_toml( "extend-exclude", "Config key extend-exclude must be a string" ) + line_ranges = config.get("line_ranges") + if line_ranges is not None: + raise click.BadOptionUsage( + "line-ranges", "Cannot use line-ranges in the pyproject.toml file." + ) + default_map: Dict[str, Any] = {} if ctx.default_map: default_map.update(ctx.default_map) @@ -304,6 +312,19 @@ def validate_regex( is_flag=True, help="Don't write the files back, just output a diff for each file on stdout.", ) +@click.option( + "--line-ranges", + multiple=True, + metavar="START-END", + help=( + "When specified, _Black_ will try its best to only format these lines. This" + " option can be specified multiple times, and a union of the lines will be" + " formatted. Each range must be specified as two integers connected by a `-`:" + " `-`. The `` and `` integer indices are 1-based and" + " inclusive on both ends." + ), + default=(), +) @click.option( "--color/--no-color", is_flag=True, @@ -443,6 +464,7 @@ def main( # noqa: C901 target_version: List[TargetVersion], check: bool, diff: bool, + line_ranges: Sequence[str], color: bool, fast: bool, pyi: bool, @@ -544,6 +566,18 @@ def main( # noqa: C901 python_cell_magics=set(python_cell_magics), ) + lines: List[Tuple[int, int]] = [] + if line_ranges: + if ipynb: + err("Cannot use --line-ranges with ipynb files.") + ctx.exit(1) + + try: + lines = parse_line_ranges(line_ranges) + except ValueError as e: + err(str(e)) + ctx.exit(1) + if code is not None: # Run in quiet mode by default with -c; the extra output isn't useful. # You can still pass -v to get verbose output. @@ -553,7 +587,12 @@ def main( # noqa: C901 if code is not None: reformat_code( - content=code, fast=fast, write_back=write_back, mode=mode, report=report + content=code, + fast=fast, + write_back=write_back, + mode=mode, + report=report, + lines=lines, ) else: assert root is not None # root is only None if code is not None @@ -588,10 +627,14 @@ def main( # noqa: C901 write_back=write_back, mode=mode, report=report, + lines=lines, ) else: from black.concurrency import reformat_many + if lines: + err("Cannot use --line-ranges to format multiple files.") + ctx.exit(1) reformat_many( sources=sources, fast=fast, @@ -714,7 +757,13 @@ def path_empty( def reformat_code( - content: str, fast: bool, write_back: WriteBack, mode: Mode, report: Report + content: str, + fast: bool, + write_back: WriteBack, + mode: Mode, + report: Report, + *, + lines: Collection[Tuple[int, int]] = (), ) -> None: """ Reformat and print out `content` without spawning child processes. @@ -727,7 +776,7 @@ def reformat_code( try: changed = Changed.NO if format_stdin_to_stdout( - content=content, fast=fast, write_back=write_back, mode=mode + content=content, fast=fast, write_back=write_back, mode=mode, lines=lines ): changed = Changed.YES report.done(path, changed) @@ -741,7 +790,13 @@ def reformat_code( # not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 @mypyc_attr(patchable=True) def reformat_one( - src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report" + src: Path, + fast: bool, + write_back: WriteBack, + mode: Mode, + report: "Report", + *, + lines: Collection[Tuple[int, int]] = (), ) -> None: """Reformat a single file under `src` without spawning child processes. @@ -766,7 +821,9 @@ def reformat_one( mode = replace(mode, is_pyi=True) elif src.suffix == ".ipynb": mode = replace(mode, is_ipynb=True) - if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode): + if format_stdin_to_stdout( + fast=fast, write_back=write_back, mode=mode, lines=lines + ): changed = Changed.YES else: cache = Cache.read(mode) @@ -774,7 +831,7 @@ def reformat_one( if not cache.is_changed(src): changed = Changed.CACHED if changed is not Changed.CACHED and format_file_in_place( - src, fast=fast, write_back=write_back, mode=mode + src, fast=fast, write_back=write_back, mode=mode, lines=lines ): changed = Changed.YES if (write_back is WriteBack.YES and changed is not Changed.CACHED) or ( @@ -794,6 +851,8 @@ def format_file_in_place( mode: Mode, write_back: WriteBack = WriteBack.NO, lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy + *, + lines: Collection[Tuple[int, int]] = (), ) -> bool: """Format file under `src` path. Return True if changed. @@ -813,7 +872,9 @@ def format_file_in_place( header = buf.readline() src_contents, encoding, newline = decode_bytes(buf.read()) try: - dst_contents = format_file_contents(src_contents, fast=fast, mode=mode) + dst_contents = format_file_contents( + src_contents, fast=fast, mode=mode, lines=lines + ) except NothingChanged: return False except JSONDecodeError: @@ -858,6 +919,7 @@ def format_stdin_to_stdout( content: Optional[str] = None, write_back: WriteBack = WriteBack.NO, mode: Mode, + lines: Collection[Tuple[int, int]] = (), ) -> bool: """Format file on stdin. Return True if changed. @@ -876,7 +938,7 @@ def format_stdin_to_stdout( dst = src try: - dst = format_file_contents(src, fast=fast, mode=mode) + dst = format_file_contents(src, fast=fast, mode=mode, lines=lines) return True except NothingChanged: @@ -904,7 +966,11 @@ def format_stdin_to_stdout( def check_stability_and_equivalence( - src_contents: str, dst_contents: str, *, mode: Mode + src_contents: str, + dst_contents: str, + *, + mode: Mode, + lines: Collection[Tuple[int, int]] = (), ) -> None: """Perform stability and equivalence checks. @@ -913,10 +979,16 @@ def check_stability_and_equivalence( content differently. """ assert_equivalent(src_contents, dst_contents) - assert_stable(src_contents, dst_contents, mode=mode) + assert_stable(src_contents, dst_contents, mode=mode, lines=lines) -def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: +def format_file_contents( + src_contents: str, + *, + fast: bool, + mode: Mode, + lines: Collection[Tuple[int, int]] = (), +) -> FileContent: """Reformat contents of a file and return new contents. If `fast` is False, additionally confirm that the reformatted code is @@ -926,13 +998,15 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo if mode.is_ipynb: dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode) else: - dst_contents = format_str(src_contents, mode=mode) + dst_contents = format_str(src_contents, mode=mode, lines=lines) if src_contents == dst_contents: raise NothingChanged if not fast and not mode.is_ipynb: # Jupyter notebooks will already have been checked above. - check_stability_and_equivalence(src_contents, dst_contents, mode=mode) + check_stability_and_equivalence( + src_contents, dst_contents, mode=mode, lines=lines + ) return dst_contents @@ -1043,7 +1117,9 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon raise NothingChanged -def format_str(src_contents: str, *, mode: Mode) -> str: +def format_str( + src_contents: str, *, mode: Mode, lines: Collection[Tuple[int, int]] = () +) -> str: """Reformat a string and return new contents. `mode` determines formatting options, such as how many characters per line are @@ -1073,16 +1149,20 @@ def f( hey """ - dst_contents = _format_str_once(src_contents, mode=mode) + dst_contents = _format_str_once(src_contents, mode=mode, lines=lines) # 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) + if lines: + lines = adjusted_lines(lines, src_contents, dst_contents) + return _format_str_once(dst_contents, mode=mode, lines=lines) return dst_contents -def _format_str_once(src_contents: str, *, mode: Mode) -> str: +def _format_str_once( + src_contents: str, *, mode: Mode, lines: Collection[Tuple[int, int]] = () +) -> str: src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) dst_blocks: List[LinesBlock] = [] if mode.target_versions: @@ -1097,7 +1177,11 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: if supports_feature(versions, feature) } normalize_fmt_off(src_node, mode) - lines = LineGenerator(mode=mode, features=context_manager_features) + if lines: + # This should be called after normalize_fmt_off. + convert_unchanged_lines(src_node, lines) + + line_generator = LineGenerator(mode=mode, features=context_manager_features) elt = EmptyLineTracker(mode=mode) split_line_features = { feature @@ -1105,7 +1189,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str: if supports_feature(versions, feature) } block: Optional[LinesBlock] = None - for current_line in lines.visit(src_node): + for current_line in line_generator.visit(src_node): block = elt.maybe_empty_lines(current_line) dst_blocks.append(block) for line in transform_line( @@ -1373,12 +1457,16 @@ def assert_equivalent(src: str, dst: str) -> None: ) from None -def assert_stable(src: str, dst: str, mode: Mode) -> None: +def assert_stable( + src: str, dst: str, mode: Mode, *, lines: Collection[Tuple[int, int]] = () +) -> None: """Raise AssertionError if `dst` reformats differently the second time.""" # 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 lines: + lines = adjusted_lines(lines, src, dst) + newdst = _format_str_once(dst, mode=mode, lines=lines) if dst != newdst: log = dump_to_file( str(mode), diff --git a/src/black/nodes.py b/src/black/nodes.py index fff8e05a118..9251b0defb0 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -935,3 +935,31 @@ def is_part_of_annotation(leaf: Leaf) -> bool: return True ancestor = ancestor.parent return False + + +def first_leaf(node: LN) -> Optional[Leaf]: + """Returns the first leaf of the ancestor node.""" + if isinstance(node, Leaf): + return node + elif not node.children: + return None + else: + return first_leaf(node.children[0]) + + +def last_leaf(node: LN) -> Optional[Leaf]: + """Returns the last leaf of the ancestor node.""" + if isinstance(node, Leaf): + return node + elif not node.children: + return None + else: + return last_leaf(node.children[-1]) + + +def furthest_ancestor_with_last_leaf(leaf: Leaf) -> LN: + """Returns the furthest ancestor that has this leaf node as the last leaf.""" + node: LN = leaf + while node.parent and node.parent.children and node is node.parent.children[-1]: + node = node.parent + return node diff --git a/src/black/ranges.py b/src/black/ranges.py new file mode 100644 index 00000000000..b0c312e6274 --- /dev/null +++ b/src/black/ranges.py @@ -0,0 +1,496 @@ +"""Functions related to Black's formatting by line ranges feature.""" + +import difflib +from dataclasses import dataclass +from typing import Collection, Iterator, List, Sequence, Set, Tuple, Union + +from black.nodes import ( + LN, + STANDALONE_COMMENT, + Leaf, + Node, + Visitor, + first_leaf, + furthest_ancestor_with_last_leaf, + last_leaf, + syms, +) +from blib2to3.pgen2.token import ASYNC, NEWLINE + + +def parse_line_ranges(line_ranges: Sequence[str]) -> List[Tuple[int, int]]: + lines: List[Tuple[int, int]] = [] + for lines_str in line_ranges: + parts = lines_str.split("-") + if len(parts) != 2: + raise ValueError( + "Incorrect --line-ranges format, expect 'START-END', found" + f" {lines_str!r}" + ) + try: + start = int(parts[0]) + end = int(parts[1]) + except ValueError: + raise ValueError( + "Incorrect --line-ranges value, expect integer ranges, found" + f" {lines_str!r}" + ) from None + else: + lines.append((start, end)) + return lines + + +def is_valid_line_range(lines: Tuple[int, int]) -> bool: + """Returns whether the line range is valid.""" + return not lines or lines[0] <= lines[1] + + +def adjusted_lines( + lines: Collection[Tuple[int, int]], + original_source: str, + modified_source: str, +) -> List[Tuple[int, int]]: + """Returns the adjusted line ranges based on edits from the original code. + + This computes the new line ranges by diffing original_source and + modified_source, and adjust each range based on how the range overlaps with + the diffs. + + Note the diff can contain lines outside of the original line ranges. This can + happen when the formatting has to be done in adjacent to maintain consistent + local results. For example: + + 1. def my_func(arg1, arg2, + 2. arg3,): + 3. pass + + If it restricts to line 2-2, it can't simply reformat line 2, it also has + to reformat line 1: + + 1. def my_func( + 2. arg1, + 3. arg2, + 4. arg3, + 5. ): + 6. pass + + In this case, we will expand the line ranges to also include the whole diff + block. + + Args: + lines: a collection of line ranges. + original_source: the original source. + modified_source: the modified source. + """ + lines_mappings = _calculate_lines_mappings(original_source, modified_source) + + new_lines = [] + # Keep an index of the current search. Since the lines and lines_mappings are + # sorted, this makes the search complexity linear. + current_mapping_index = 0 + for start, end in sorted(lines): + start_mapping_index = _find_lines_mapping_index( + start, + lines_mappings, + current_mapping_index, + ) + end_mapping_index = _find_lines_mapping_index( + end, + lines_mappings, + start_mapping_index, + ) + current_mapping_index = start_mapping_index + if start_mapping_index >= len(lines_mappings) or end_mapping_index >= len( + lines_mappings + ): + # Protect against invalid inputs. + continue + start_mapping = lines_mappings[start_mapping_index] + end_mapping = lines_mappings[end_mapping_index] + if start_mapping.is_changed_block: + # When the line falls into a changed block, expands to the whole block. + new_start = start_mapping.modified_start + else: + new_start = ( + start - start_mapping.original_start + start_mapping.modified_start + ) + if end_mapping.is_changed_block: + # When the line falls into a changed block, expands to the whole block. + new_end = end_mapping.modified_end + else: + new_end = end - end_mapping.original_start + end_mapping.modified_start + new_range = (new_start, new_end) + if is_valid_line_range(new_range): + new_lines.append(new_range) + return new_lines + + +def convert_unchanged_lines(src_node: Node, lines: Collection[Tuple[int, int]]) -> None: + """Converts unchanged lines to STANDALONE_COMMENT. + + The idea is similar to how `# fmt: on/off` is implemented. It also converts the + nodes between those markers as a single `STANDALONE_COMMENT` leaf node with + the unformatted code as its value. `STANDALONE_COMMENT` is a "fake" token + that will be formatted as-is with its prefix normalized. + + Here we perform two passes: + + 1. Visit the top-level statements, and convert them to a single + `STANDALONE_COMMENT` when unchanged. This speeds up formatting when some + of the top-level statements aren't changed. + 2. Convert unchanged "unwrapped lines" to `STANDALONE_COMMENT` nodes line by + line. "unwrapped lines" are divided by the `NEWLINE` token. e.g. a + multi-line statement is *one* "unwrapped line" that ends with `NEWLINE`, + even though this statement itself can span multiple lines, and the + tokenizer only sees the last '\n' as the `NEWLINE` token. + + NOTE: During pass (2), comment prefixes and indentations are ALWAYS + normalized even when the lines aren't changed. This is fixable by moving + more formatting to pass (1). However, it's hard to get it correct when + incorrect indentations are used. So we defer this to future optimizations. + """ + lines_set: Set[int] = set() + for start, end in lines: + lines_set.update(range(start, end + 1)) + visitor = _TopLevelStatementsVisitor(lines_set) + _ = list(visitor.visit(src_node)) # Consume all results. + _convert_unchanged_line_by_line(src_node, lines_set) + + +def _contains_standalone_comment(node: LN) -> bool: + if isinstance(node, Leaf): + return node.type == STANDALONE_COMMENT + else: + for child in node.children: + if _contains_standalone_comment(child): + return True + return False + + +class _TopLevelStatementsVisitor(Visitor[None]): + """ + A node visitor that converts unchanged top-level statements to + STANDALONE_COMMENT. + + This is used in addition to _convert_unchanged_lines_by_flatterning, to + speed up formatting when there are unchanged top-level + classes/functions/statements. + """ + + def __init__(self, lines_set: Set[int]): + self._lines_set = lines_set + + def visit_simple_stmt(self, node: Node) -> Iterator[None]: + # This is only called for top-level statements, since `visit_suite` + # won't visit its children nodes. + yield from [] + newline_leaf = last_leaf(node) + if not newline_leaf: + return + assert ( + newline_leaf.type == NEWLINE + ), f"Unexpectedly found leaf.type={newline_leaf.type}" + # We need to find the furthest ancestor with the NEWLINE as the last + # leaf, since a `suite` can simply be a `simple_stmt` when it puts + # its body on the same line. Example: `if cond: pass`. + ancestor = furthest_ancestor_with_last_leaf(newline_leaf) + if not _get_line_range(ancestor).intersection(self._lines_set): + _convert_node_to_standalone_comment(ancestor) + + def visit_suite(self, node: Node) -> Iterator[None]: + yield from [] + # If there is a STANDALONE_COMMENT node, it means parts of the node tree + # have fmt on/off/skip markers. Those STANDALONE_COMMENT nodes can't + # be simply converted by calling str(node). So we just don't convert + # here. + if _contains_standalone_comment(node): + return + # Find the semantic parent of this suite. For `async_stmt` and + # `async_funcdef`, the ASYNC token is defined on a separate level by the + # grammar. + semantic_parent = node.parent + if semantic_parent is not None: + if ( + semantic_parent.prev_sibling is not None + and semantic_parent.prev_sibling.type == ASYNC + ): + semantic_parent = semantic_parent.parent + if semantic_parent is not None and not _get_line_range( + semantic_parent + ).intersection(self._lines_set): + _convert_node_to_standalone_comment(semantic_parent) + + +def _convert_unchanged_line_by_line(node: Node, lines_set: Set[int]) -> None: + """Converts unchanged to STANDALONE_COMMENT line by line.""" + for leaf in node.leaves(): + if leaf.type != NEWLINE: + # We only consider "unwrapped lines", which are divided by the NEWLINE + # token. + continue + if leaf.parent and leaf.parent.type == syms.match_stmt: + # The `suite` node is defined as: + # match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT + # Here we need to check `subject_expr`. The `case_block+` will be + # checked by their own NEWLINEs. + nodes_to_ignore: List[LN] = [] + prev_sibling = leaf.prev_sibling + while prev_sibling: + nodes_to_ignore.insert(0, prev_sibling) + prev_sibling = prev_sibling.prev_sibling + if not _get_line_range(nodes_to_ignore).intersection(lines_set): + _convert_nodes_to_standalone_comment(nodes_to_ignore, newline=leaf) + elif leaf.parent and leaf.parent.type == syms.suite: + # The `suite` node is defined as: + # suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT + # We will check `simple_stmt` and `stmt+` separately against the lines set + parent_sibling = leaf.parent.prev_sibling + nodes_to_ignore = [] + while parent_sibling and not parent_sibling.type == syms.suite: + # NOTE: Multiple suite nodes can exist as siblings in e.g. `if_stmt`. + nodes_to_ignore.insert(0, parent_sibling) + parent_sibling = parent_sibling.prev_sibling + # Special case for `async_stmt` and `async_funcdef` where the ASYNC + # token is on the grandparent node. + grandparent = leaf.parent.parent + if ( + grandparent is not None + and grandparent.prev_sibling is not None + and grandparent.prev_sibling.type == ASYNC + ): + nodes_to_ignore.insert(0, grandparent.prev_sibling) + if not _get_line_range(nodes_to_ignore).intersection(lines_set): + _convert_nodes_to_standalone_comment(nodes_to_ignore, newline=leaf) + else: + ancestor = furthest_ancestor_with_last_leaf(leaf) + # Consider multiple decorators as a whole block, as their + # newlines have different behaviors than the rest of the grammar. + if ( + ancestor.type == syms.decorator + and ancestor.parent + and ancestor.parent.type == syms.decorators + ): + ancestor = ancestor.parent + if not _get_line_range(ancestor).intersection(lines_set): + _convert_node_to_standalone_comment(ancestor) + + +def _convert_node_to_standalone_comment(node: LN) -> None: + """Convert node to STANDALONE_COMMENT by modifying the tree inline.""" + parent = node.parent + if not parent: + return + first = first_leaf(node) + last = last_leaf(node) + if not first or not last: + return + if first is last: + # This can happen on the following edge cases: + # 1. A block of `# fmt: off/on` code except the `# fmt: on` is placed + # on the end of the last line instead of on a new line. + # 2. A single backslash on its own line followed by a comment line. + # Ideally we don't want to format them when not requested, but fixing + # isn't easy. These cases are also badly formatted code, so it isn't + # too bad we reformat them. + return + # The prefix contains comments and indentation whitespaces. They are + # reformatted accordingly to the correct indentation level. + # This also means the indentation will be changed on the unchanged lines, and + # this is actually required to not break incremental reformatting. + prefix = first.prefix + first.prefix = "" + index = node.remove() + if index is not None: + # Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when + # genearting the formatted code. + value = str(node)[:-1] + parent.insert_child( + index, + Leaf( + STANDALONE_COMMENT, + value, + prefix=prefix, + fmt_pass_converted_first_leaf=first, + ), + ) + + +def _convert_nodes_to_standalone_comment(nodes: Sequence[LN], *, newline: Leaf) -> None: + """Convert nodes to STANDALONE_COMMENT by modifying the tree inline.""" + if not nodes: + return + parent = nodes[0].parent + first = first_leaf(nodes[0]) + if not parent or not first: + return + prefix = first.prefix + first.prefix = "" + value = "".join(str(node) for node in nodes) + # The prefix comment on the NEWLINE leaf is the trailing comment of the statement. + if newline.prefix: + value += newline.prefix + newline.prefix = "" + index = nodes[0].remove() + for node in nodes[1:]: + node.remove() + if index is not None: + parent.insert_child( + index, + Leaf( + STANDALONE_COMMENT, + value, + prefix=prefix, + fmt_pass_converted_first_leaf=first, + ), + ) + + +def _leaf_line_end(leaf: Leaf) -> int: + """Returns the line number of the leaf node's last line.""" + if leaf.type == NEWLINE: + return leaf.lineno + else: + # Leaf nodes like multiline strings can occupy multiple lines. + return leaf.lineno + str(leaf).count("\n") + + +def _get_line_range(node_or_nodes: Union[LN, List[LN]]) -> Set[int]: + """Returns the line range of this node or list of nodes.""" + if isinstance(node_or_nodes, list): + nodes = node_or_nodes + if not nodes: + return set() + first = first_leaf(nodes[0]) + last = last_leaf(nodes[-1]) + if first and last: + line_start = first.lineno + line_end = _leaf_line_end(last) + return set(range(line_start, line_end + 1)) + else: + return set() + else: + node = node_or_nodes + if isinstance(node, Leaf): + return set(range(node.lineno, _leaf_line_end(node) + 1)) + else: + first = first_leaf(node) + last = last_leaf(node) + if first and last: + return set(range(first.lineno, _leaf_line_end(last) + 1)) + else: + return set() + + +@dataclass +class _LinesMapping: + """1-based lines mapping from original source to modified source. + + Lines [original_start, original_end] from original source + are mapped to [modified_start, modified_end]. + + The ranges are inclusive on both ends. + """ + + original_start: int + original_end: int + modified_start: int + modified_end: int + # Whether this range corresponds to a changed block, or an unchanged block. + is_changed_block: bool + + +def _calculate_lines_mappings( + original_source: str, + modified_source: str, +) -> Sequence[_LinesMapping]: + """Returns a sequence of _LinesMapping by diffing the sources. + + For example, given the following diff: + import re + - def func(arg1, + - arg2, arg3): + + def func(arg1, arg2, arg3): + pass + It returns the following mappings: + original -> modified + (1, 1) -> (1, 1), is_changed_block=False (the "import re" line) + (2, 3) -> (2, 2), is_changed_block=True (the diff) + (4, 4) -> (3, 3), is_changed_block=False (the "pass" line) + + You can think of this visually as if it brings up a side-by-side diff, and tries + to map the line ranges from the left side to the right side: + + (1, 1)->(1, 1) 1. import re 1. import re + (2, 3)->(2, 2) 2. def func(arg1, 2. def func(arg1, arg2, arg3): + 3. arg2, arg3): + (4, 4)->(3, 3) 4. pass 3. pass + + Args: + original_source: the original source. + modified_source: the modified source. + """ + matcher = difflib.SequenceMatcher( + None, + original_source.splitlines(keepends=True), + modified_source.splitlines(keepends=True), + ) + matching_blocks = matcher.get_matching_blocks() + lines_mappings: List[_LinesMapping] = [] + # matching_blocks is a sequence of "same block of code ranges", see + # https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.get_matching_blocks + # Each block corresponds to a _LinesMapping with is_changed_block=False, + # and the ranges between two blocks corresponds to a _LinesMapping with + # is_changed_block=True, + # NOTE: matching_blocks is 0-based, but _LinesMapping is 1-based. + for i, block in enumerate(matching_blocks): + if i == 0: + if block.a != 0 or block.b != 0: + lines_mappings.append( + _LinesMapping( + original_start=1, + original_end=block.a, + modified_start=1, + modified_end=block.b, + is_changed_block=False, + ) + ) + else: + previous_block = matching_blocks[i - 1] + lines_mappings.append( + _LinesMapping( + original_start=previous_block.a + previous_block.size + 1, + original_end=block.a, + modified_start=previous_block.b + previous_block.size + 1, + modified_end=block.b, + is_changed_block=True, + ) + ) + if i < len(matching_blocks) - 1: + lines_mappings.append( + _LinesMapping( + original_start=block.a + 1, + original_end=block.a + block.size, + modified_start=block.b + 1, + modified_end=block.b + block.size, + is_changed_block=False, + ) + ) + return lines_mappings + + +def _find_lines_mapping_index( + original_line: int, + lines_mappings: Sequence[_LinesMapping], + start_index: int, +) -> int: + """Returns the original index of the lines mappings for the original line.""" + index = start_index + while index < len(lines_mappings): + mapping = lines_mappings[index] + if ( + mapping.original_start <= original_line + and original_line <= mapping.original_end + ): + return index + index += 1 + return index diff --git a/tests/data/cases/line_ranges_basic.py b/tests/data/cases/line_ranges_basic.py new file mode 100644 index 00000000000..9f0fb2da70e --- /dev/null +++ b/tests/data/cases/line_ranges_basic.py @@ -0,0 +1,107 @@ +# flags: --line-ranges=5-6 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass + +# Adding some unformated code covering a wide range of syntaxes. + +if True: + # Incorrectly indented prefix comments. + pass + +import typing +from typing import ( + Any , + ) +class MyClass( object): # Trailing comment with extra leading space. + #NOTE: The following indentation is incorrect: + @decor( 1 * 3 ) + def my_func( arg): + pass + +try: # Trailing comment with extra leading space. + for i in range(10): # Trailing comment with extra leading space. + while condition: + if something: + then_something( ) + elif something_else: + then_something_else( ) +except ValueError as e: + unformatted( ) +finally: + unformatted( ) + +async def test_async_unformatted( ): # Trailing comment with extra leading space. + async for i in some_iter( unformatted ): # Trailing comment with extra leading space. + await asyncio.sleep( 1 ) + async with some_context( unformatted ): + print( "unformatted" ) + + +# output +# flags: --line-ranges=5-6 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo2( + parameter_1, + parameter_2, + parameter_3, + parameter_4, + parameter_5, + parameter_6, + parameter_7, +): + pass + + +def foo3( + parameter_1, + parameter_2, + parameter_3, + parameter_4, + parameter_5, + parameter_6, + parameter_7, +): + pass + + +def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass + +# Adding some unformated code covering a wide range of syntaxes. + +if True: + # Incorrectly indented prefix comments. + pass + +import typing +from typing import ( + Any , + ) +class MyClass( object): # Trailing comment with extra leading space. + #NOTE: The following indentation is incorrect: + @decor( 1 * 3 ) + def my_func( arg): + pass + +try: # Trailing comment with extra leading space. + for i in range(10): # Trailing comment with extra leading space. + while condition: + if something: + then_something( ) + elif something_else: + then_something_else( ) +except ValueError as e: + unformatted( ) +finally: + unformatted( ) + +async def test_async_unformatted( ): # Trailing comment with extra leading space. + async for i in some_iter( unformatted ): # Trailing comment with extra leading space. + await asyncio.sleep( 1 ) + async with some_context( unformatted ): + print( "unformatted" ) diff --git a/tests/data/cases/line_ranges_fmt_off.py b/tests/data/cases/line_ranges_fmt_off.py new file mode 100644 index 00000000000..b51cef58fe5 --- /dev/null +++ b/tests/data/cases/line_ranges_fmt_off.py @@ -0,0 +1,49 @@ +# flags: --line-ranges=7-7 --line-ranges=17-23 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# fmt: off +import os +def myfunc( ): # Intentionally unformatted. + pass +# fmt: on + + +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: off +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: on + + +def myfunc( ): # This will be reformatted. + print( {"this will be reformatted"} ) + +# output + +# flags: --line-ranges=7-7 --line-ranges=17-23 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# fmt: off +import os +def myfunc( ): # Intentionally unformatted. + pass +# fmt: on + + +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: off +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: on + + +def myfunc(): # This will be reformatted. + print({"this will be reformatted"}) diff --git a/tests/data/cases/line_ranges_fmt_off_decorator.py b/tests/data/cases/line_ranges_fmt_off_decorator.py new file mode 100644 index 00000000000..14aa1dda02d --- /dev/null +++ b/tests/data/cases/line_ranges_fmt_off_decorator.py @@ -0,0 +1,27 @@ +# flags: --line-ranges=12-12 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Regression test for an edge case involving decorators and fmt: off/on. +class MyClass: + + # fmt: off + @decorator ( ) + # fmt: on + def method(): + print ( "str" ) + +# output + +# flags: --line-ranges=12-12 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Regression test for an edge case involving decorators and fmt: off/on. +class MyClass: + + # fmt: off + @decorator ( ) + # fmt: on + def method(): + print("str") diff --git a/tests/data/cases/line_ranges_fmt_off_overlap.py b/tests/data/cases/line_ranges_fmt_off_overlap.py new file mode 100644 index 00000000000..0391d17a843 --- /dev/null +++ b/tests/data/cases/line_ranges_fmt_off_overlap.py @@ -0,0 +1,37 @@ +# flags: --line-ranges=11-17 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + + +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: off +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: on + + +def myfunc( ): # This will be reformatted. + print( {"this will be reformatted"} ) + +# output + +# flags: --line-ranges=11-17 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + + +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: off +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +def myfunc( ): # This will not be reformatted. + print( {"also won't be reformatted"} ) +# fmt: on + + +def myfunc(): # This will be reformatted. + print({"this will be reformatted"}) diff --git a/tests/data/cases/line_ranges_imports.py b/tests/data/cases/line_ranges_imports.py new file mode 100644 index 00000000000..76b18ffecb3 --- /dev/null +++ b/tests/data/cases/line_ranges_imports.py @@ -0,0 +1,9 @@ +# flags: --line-ranges=8-8 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# This test ensures no empty lines are added around import lines. +# It caused an issue before https://github.com/psf/black/pull/3610 is merged. +import os +import re +import sys diff --git a/tests/data/cases/line_ranges_indentation.py b/tests/data/cases/line_ranges_indentation.py new file mode 100644 index 00000000000..82d3ad69a5e --- /dev/null +++ b/tests/data/cases/line_ranges_indentation.py @@ -0,0 +1,27 @@ +# flags: --line-ranges=5-5 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +if cond1: + print("first") + if cond2: + print("second") + else: + print("else") + +if another_cond: + print("will not be changed") + +# output + +# flags: --line-ranges=5-5 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +if cond1: + print("first") + if cond2: + print("second") + else: + print("else") + +if another_cond: + print("will not be changed") diff --git a/tests/data/cases/line_ranges_two_passes.py b/tests/data/cases/line_ranges_two_passes.py new file mode 100644 index 00000000000..aeed3260b8e --- /dev/null +++ b/tests/data/cases/line_ranges_two_passes.py @@ -0,0 +1,27 @@ +# flags: --line-ranges=9-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# This is a specific case for Black's two-pass formatting behavior in `format_str`. +# The second pass must respect the line ranges before the first pass. + + +def restrict_to_this_line(arg1, + arg2, + arg3): + print ( "This should not be formatted." ) + print ( "Note that in the second pass, the original line range 9-11 will cover these print lines.") + +# output + +# flags: --line-ranges=9-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# This is a specific case for Black's two-pass formatting behavior in `format_str`. +# The second pass must respect the line ranges before the first pass. + + +def restrict_to_this_line(arg1, arg2, arg3): + print ( "This should not be formatted." ) + print ( "Note that in the second pass, the original line range 9-11 will cover these print lines.") diff --git a/tests/data/cases/line_ranges_unwrapping.py b/tests/data/cases/line_ranges_unwrapping.py new file mode 100644 index 00000000000..cd7751b9417 --- /dev/null +++ b/tests/data/cases/line_ranges_unwrapping.py @@ -0,0 +1,25 @@ +# flags: --line-ranges=5-5 --line-ranges=9-9 --line-ranges=13-13 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +alist = [ + 1, 2 +] + +adict = { + "key" : "value" +} + +func_call ( + arg = value +) + +# output + +# flags: --line-ranges=5-5 --line-ranges=9-9 --line-ranges=13-13 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +alist = [1, 2] + +adict = {"key": "value"} + +func_call(arg=value) diff --git a/tests/data/invalid_line_ranges.toml b/tests/data/invalid_line_ranges.toml new file mode 100644 index 00000000000..791573f2625 --- /dev/null +++ b/tests/data/invalid_line_ranges.toml @@ -0,0 +1,2 @@ +[tool.black] +line-ranges = "1-1" diff --git a/tests/data/line_ranges_formatted/basic.py b/tests/data/line_ranges_formatted/basic.py new file mode 100644 index 00000000000..b419b1f16ae --- /dev/null +++ b/tests/data/line_ranges_formatted/basic.py @@ -0,0 +1,50 @@ +"""Module doc.""" + +from typing import ( + Callable, + Literal, +) + + +# fmt: off +class Unformatted: + def should_also_work(self): + pass +# fmt: on + + +a = [1, 2] # fmt: skip + + +# This should cover as many syntaxes as possible. +class Foo: + """Class doc.""" + + def __init__(self) -> None: + pass + + @add_logging + @memoize.memoize(max_items=2) + def plus_one( + self, + number: int, + ) -> int: + return number + 1 + + async def async_plus_one(self, number: int) -> int: + await asyncio.sleep(1) + async with some_context(): + return number + 1 + + +try: + for i in range(10): + while condition: + if something: + then_something() + elif something_else: + then_something_else() +except ValueError as e: + handle(e) +finally: + done() diff --git a/tests/data/line_ranges_formatted/pattern_matching.py b/tests/data/line_ranges_formatted/pattern_matching.py new file mode 100644 index 00000000000..cd98efdd504 --- /dev/null +++ b/tests/data/line_ranges_formatted/pattern_matching.py @@ -0,0 +1,25 @@ +# flags: --minimum-version=3.10 + + +def pattern_matching(): + match status: + case 1: + return "1" + case [single]: + return "single" + case [ + action, + obj, + ]: + return "act on obj" + case Point(x=0): + return "class pattern" + case {"text": message}: + return "mapping" + case { + "text": message, + "format": _, + }: + return "mapping" + case _: + return "fallback" diff --git a/tests/test_black.py b/tests/test_black.py index c7196098e14..c9819742425 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -8,6 +8,7 @@ import os import re import sys +import textwrap import types from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager, redirect_stderr @@ -1269,7 +1270,7 @@ def test_reformat_one_with_stdin_filename(self) -> None: report=report, ) fsts.assert_called_once_with( - fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE + fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE, lines=() ) # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) @@ -1295,6 +1296,7 @@ def test_reformat_one_with_stdin_filename_pyi(self) -> None: fast=True, write_back=black.WriteBack.YES, mode=replace(DEFAULT_MODE, is_pyi=True), + lines=(), ) # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) @@ -1320,6 +1322,7 @@ def test_reformat_one_with_stdin_filename_ipynb(self) -> None: fast=True, write_back=black.WriteBack.YES, mode=replace(DEFAULT_MODE, is_ipynb=True), + lines=(), ) # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) @@ -1941,6 +1944,88 @@ def test_equivalency_ast_parse_failure_includes_error(self) -> None: err.match("invalid character") err.match(r"\(, line 1\)") + def test_line_ranges_with_code_option(self) -> None: + code = textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """) + args = ["--line-ranges=1-1", "--code", code] + result = CliRunner().invoke(black.main, args) + + expected = textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """) + self.compare_results(result, expected, expected_exit_code=0) + + def test_line_ranges_with_stdin(self) -> None: + code = textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """) + runner = BlackRunner() + result = runner.invoke( + black.main, ["--line-ranges=1-1", "-"], input=BytesIO(code.encode("utf-8")) + ) + + expected = textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """) + self.compare_results(result, expected, expected_exit_code=0) + + def test_line_ranges_with_source(self) -> None: + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "test.py" + test_file.write_text( + textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """), + encoding="utf-8", + ) + args = ["--line-ranges=1-1", str(test_file)] + result = CliRunner().invoke(black.main, args) + assert not result.exit_code + + formatted = test_file.read_text(encoding="utf-8") + expected = textwrap.dedent("""\ + if a == b: + print ( "OK" ) + """) + assert expected == formatted + + def test_line_ranges_with_multiple_sources(self) -> None: + with TemporaryDirectory() as workspace: + test1_file = Path(workspace) / "test1.py" + test1_file.write_text("", encoding="utf-8") + test2_file = Path(workspace) / "test2.py" + test2_file.write_text("", encoding="utf-8") + args = ["--line-ranges=1-1", str(test1_file), str(test2_file)] + result = CliRunner().invoke(black.main, args) + assert result.exit_code == 1 + assert "Cannot use --line-ranges to format multiple files" in result.output + + def test_line_ranges_with_ipynb(self) -> None: + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "test.ipynb" + test_file.write_text("{}", encoding="utf-8") + args = ["--line-ranges=1-1", "--ipynb", str(test_file)] + result = CliRunner().invoke(black.main, args) + assert "Cannot use --line-ranges with ipynb files" in result.output + assert result.exit_code == 1 + + def test_line_ranges_in_pyproject_toml(self) -> None: + config = THIS_DIR / "data" / "invalid_line_ranges.toml" + result = BlackRunner().invoke( + black.main, ["--code", "print()", "--config", str(config)] + ) + assert result.exit_code == 2 + assert result.stderr_bytes is not None + assert ( + b"Cannot use line-ranges in the pyproject.toml file." in result.stderr_bytes + ) + class TestCaching: def test_get_cache_dir( diff --git a/tests/test_format.py b/tests/test_format.py index 4e863c6c54b..6c2eca8c618 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -29,13 +29,19 @@ def check_file(subdir: str, filename: str, *, data: bool = True) -> None: args.mode, fast=args.fast, minimum_version=args.minimum_version, + lines=args.lines, ) if args.minimum_version is not None: major, minor = args.minimum_version target_version = TargetVersion[f"PY{major}{minor}"] mode = replace(args.mode, target_versions={target_version}) assert_format( - source, expected, mode, fast=args.fast, minimum_version=args.minimum_version + source, + expected, + mode, + fast=args.fast, + minimum_version=args.minimum_version, + lines=args.lines, ) @@ -45,6 +51,24 @@ def test_simple_format(filename: str) -> None: check_file("cases", filename) +@pytest.mark.parametrize("filename", all_data_cases("line_ranges_formatted")) +def test_line_ranges_line_by_line(filename: str) -> None: + args, source, expected = read_data_with_mode("line_ranges_formatted", filename) + assert ( + source == expected + ), "Test cases in line_ranges_formatted must already be formatted." + line_count = len(source.splitlines()) + for line in range(1, line_count + 1): + assert_format( + source, + expected, + args.mode, + fast=args.fast, + minimum_version=args.minimum_version, + lines=[(line, line)], + ) + + # =============== # # Unusual cases # =============== # diff --git a/tests/test_ranges.py b/tests/test_ranges.py new file mode 100644 index 00000000000..d9fa9171a7f --- /dev/null +++ b/tests/test_ranges.py @@ -0,0 +1,185 @@ +"""Test the black.ranges module.""" + +from typing import List, Tuple + +import pytest + +from black.ranges import adjusted_lines + + +@pytest.mark.parametrize( + "lines", + [[(1, 1)], [(1, 3)], [(1, 1), (3, 4)]], +) +def test_no_diff(lines: List[Tuple[int, int]]) -> None: + source = """\ +import re + +def func(): +pass +""" + assert lines == adjusted_lines(lines, source, source) + + +@pytest.mark.parametrize( + "lines", + [ + [(1, 0)], + [(-8, 0)], + [(-8, 8)], + [(1, 100)], + [(2, 1)], + [(0, 8), (3, 1)], + ], +) +def test_invalid_lines(lines: List[Tuple[int, int]]) -> None: + original_source = """\ +import re +def foo(arg): +'''This is the foo function. + +This is foo function's +docstring with more descriptive texts. +''' + +def func(arg1, +arg2, arg3): +pass +""" + modified_source = """\ +import re +def foo(arg): +'''This is the foo function. + +This is foo function's +docstring with more descriptive texts. +''' + +def func(arg1, arg2, arg3): +pass +""" + assert not adjusted_lines(lines, original_source, modified_source) + + +@pytest.mark.parametrize( + "lines,adjusted", + [ + ( + [(1, 1)], + [(1, 1)], + ), + ( + [(1, 2)], + [(1, 1)], + ), + ( + [(1, 6)], + [(1, 2)], + ), + ( + [(6, 6)], + [], + ), + ], +) +def test_removals( + lines: List[Tuple[int, int]], adjusted: List[Tuple[int, int]] +) -> None: + original_source = """\ +1. first line +2. second line +3. third line +4. fourth line +5. fifth line +6. sixth line +""" + modified_source = """\ +2. second line +5. fifth line +""" + assert adjusted == adjusted_lines(lines, original_source, modified_source) + + +@pytest.mark.parametrize( + "lines,adjusted", + [ + ( + [(1, 1)], + [(2, 2)], + ), + ( + [(1, 2)], + [(2, 5)], + ), + ( + [(2, 2)], + [(5, 5)], + ), + ], +) +def test_additions( + lines: List[Tuple[int, int]], adjusted: List[Tuple[int, int]] +) -> None: + original_source = """\ +1. first line +2. second line +""" + modified_source = """\ +this is added +1. first line +this is added +this is added +2. second line +this is added +""" + assert adjusted == adjusted_lines(lines, original_source, modified_source) + + +@pytest.mark.parametrize( + "lines,adjusted", + [ + ( + [(1, 11)], + [(1, 10)], + ), + ( + [(1, 12)], + [(1, 11)], + ), + ( + [(10, 10)], + [(9, 9)], + ), + ([(1, 1), (9, 10)], [(1, 1), (9, 9)]), + ([(9, 10), (1, 1)], [(1, 1), (9, 9)]), + ], +) +def test_diffs(lines: List[Tuple[int, int]], adjusted: List[Tuple[int, int]]) -> None: + original_source = """\ + 1. import re + 2. def foo(arg): + 3. '''This is the foo function. + 4. + 5. This is foo function's + 6. docstring with more descriptive texts. + 7. ''' + 8. + 9. def func(arg1, +10. arg2, arg3): +11. pass +12. # last line +""" + modified_source = """\ + 1. import re # changed + 2. def foo(arg): + 3. '''This is the foo function. + 4. + 5. This is foo function's + 6. docstring with more descriptive texts. + 7. ''' + 8. + 9. def func(arg1, arg2, arg3): +11. pass +12. # last line changed +""" + assert adjusted == adjusted_lines(lines, original_source, modified_source) diff --git a/tests/util.py b/tests/util.py index a31ae0992c2..c8699d335ab 100644 --- a/tests/util.py +++ b/tests/util.py @@ -8,13 +8,14 @@ from dataclasses import dataclass, field, replace from functools import partial from pathlib import Path -from typing import Any, Iterator, List, Optional, Tuple +from typing import Any, Collection, Iterator, List, Optional, Tuple import black from black.const import DEFAULT_LINE_LENGTH from black.debug import DebugVisitor from black.mode import TargetVersion from black.output import diff, err, out +from black.ranges import parse_line_ranges from . import conftest @@ -44,6 +45,7 @@ class TestCaseArgs: mode: black.Mode = field(default_factory=black.Mode) fast: bool = False minimum_version: Optional[Tuple[int, int]] = None + lines: Collection[Tuple[int, int]] = () def _assert_format_equal(expected: str, actual: str) -> None: @@ -93,6 +95,7 @@ def assert_format( *, fast: bool = False, minimum_version: Optional[Tuple[int, int]] = None, + lines: Collection[Tuple[int, int]] = (), ) -> None: """Convenience function to check that Black formats as expected. @@ -101,7 +104,7 @@ def assert_format( separate from TargetVerson Mode configuration. """ _assert_format_inner( - source, expected, mode, fast=fast, minimum_version=minimum_version + source, expected, mode, fast=fast, minimum_version=minimum_version, lines=lines ) # For both preview and non-preview tests, ensure that Black doesn't crash on @@ -113,6 +116,7 @@ def assert_format( replace(mode, preview=not mode.preview), fast=fast, minimum_version=minimum_version, + lines=lines, ) except Exception as e: text = "non-preview" if mode.preview else "preview" @@ -129,6 +133,7 @@ def assert_format( replace(mode, preview=False, line_length=1), fast=fast, minimum_version=minimum_version, + lines=lines, ) except Exception as e: raise FormatFailure( @@ -143,8 +148,9 @@ def _assert_format_inner( *, fast: bool = False, minimum_version: Optional[Tuple[int, int]] = None, + lines: Collection[Tuple[int, int]] = (), ) -> None: - actual = black.format_str(source, mode=mode) + actual = black.format_str(source, mode=mode, lines=lines) if expected is not None: _assert_format_equal(expected, actual) # It's not useful to run safety checks if we're expecting no changes anyway. The @@ -156,7 +162,7 @@ def _assert_format_inner( # when checking modern code on older versions. if minimum_version is None or sys.version_info >= minimum_version: black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode=mode) + black.assert_stable(source, actual, mode=mode, lines=lines) def dump_to_stderr(*output: str) -> str: @@ -239,6 +245,7 @@ def get_flags_parser() -> argparse.ArgumentParser: " version works correctly." ), ) + parser.add_argument("--line-ranges", action="append") return parser @@ -254,7 +261,13 @@ def parse_mode(flags_line: str) -> TestCaseArgs: magic_trailing_comma=not args.skip_magic_trailing_comma, preview=args.preview, ) - return TestCaseArgs(mode=mode, fast=args.fast, minimum_version=args.minimum_version) + if args.line_ranges: + lines = parse_line_ranges(args.line_ranges) + else: + lines = [] + return TestCaseArgs( + mode=mode, fast=args.fast, minimum_version=args.minimum_version, lines=lines + ) def read_data_from_file(file_name: Path) -> Tuple[TestCaseArgs, str, str]: @@ -267,6 +280,12 @@ def read_data_from_file(file_name: Path) -> Tuple[TestCaseArgs, str, str]: for line in lines: if not _input and line.startswith("# flags: "): mode = parse_mode(line[len("# flags: ") :]) + if mode.lines: + # Retain the `# flags: ` line when using --line-ranges=. This requires + # the `# output` section to also include this line, but retaining the + # line is important to make the line ranges match what you see in the + # test file. + result.append(line) continue line = line.replace(EMPTY_LINE, "") if line.rstrip() == "# output": From 50ed6221d97b265025abaa66116a7b185f2df5e2 Mon Sep 17 00:00:00 2001 From: rdrll <13176405+rdrll@users.noreply.github.com> Date: Tue, 7 Nov 2023 06:31:58 -0800 Subject: [PATCH 625/700] Fix long case blocks not split into multiple lines (#4024) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + src/black/linegen.py | 24 +++++++++++- src/black/mode.py | 1 + tests/data/cases/pattern_matching_extras.py | 22 +---------- .../cases/preview_pattern_matching_long.py | 34 ++++++++++++++++ ...preview_pattern_matching_trailing_comma.py | 39 +++++++++++++++++++ 6 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tests/data/cases/preview_pattern_matching_long.py create mode 100644 tests/data/cases/preview_pattern_matching_trailing_comma.py diff --git a/CHANGES.md b/CHANGES.md index 780a00247ce..c8ba83b5ae9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ indented less (#3964) - Multiline list and dict unpacking as the sole argument to a function is now also indented less (#3992) +- Fix a bug where long `case` blocks were not split into multiple lines. Also enable + general trailing comma rules on `case` blocks (#4024) - Keep requiring two empty lines between module-level docstring and first function or class definition. (#4028) diff --git a/src/black/linegen.py b/src/black/linegen.py index b13b95d9b31..30cfff3e846 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1229,7 +1229,7 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: leaf.prefix = "" -def normalize_invisible_parens( +def normalize_invisible_parens( # noqa: C901 node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: """Make existing optional parentheses invisible or create new ones. @@ -1260,6 +1260,17 @@ def normalize_invisible_parens( child, parens_after=parens_after, mode=mode, features=features ) + # Fixes a bug where invisible parens are not properly wrapped around + # case blocks. + if ( + isinstance(child, Node) + and child.type == syms.case_block + and Preview.long_case_block_line_splitting in mode + ): + normalize_invisible_parens( + child, parens_after={"case"}, mode=mode, features=features + ) + # Add parentheses around long tuple unpacking in assignments. if ( index == 0 @@ -1305,6 +1316,17 @@ def normalize_invisible_parens( # invisible parentheses to work more precisely. continue + elif ( + isinstance(child, Leaf) + and child.next_sibling is not None + and child.next_sibling.type == token.COLON + and child.value == "case" + and Preview.long_case_block_line_splitting in mode + ): + # A special patch for "case case:" scenario, the second occurrence + # of case will be not parsed as a Python keyword. + break + elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) diff --git a/src/black/mode.py b/src/black/mode.py index 4e4effffb86..1aa5cbecc86 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -193,6 +193,7 @@ class Preview(Enum): hug_parens_with_braces_and_square_brackets = auto() allow_empty_first_line_before_new_block_or_comment = auto() single_line_format_skip_with_multiple_comments = auto() + long_case_block_line_splitting = auto() class Deprecated(UserWarning): diff --git a/tests/data/cases/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py index 1e1481d7bbe..1aef8f16b5a 100644 --- a/tests/data/cases/pattern_matching_extras.py +++ b/tests/data/cases/pattern_matching_extras.py @@ -30,22 +30,6 @@ def func(match: case, case: match) -> case: ... -match maybe, multiple: - case perhaps, 5: - pass - case perhaps, 6,: - pass - - -match more := (than, one), indeed,: - case _, (5, 6): - pass - case [[5], (6)], [7],: - pass - case _: - pass - - match a, *b, c: case [*_]: assert "seq" == _ @@ -67,12 +51,12 @@ def func(match: case, case: match) -> case: ), ): pass - case [a as match]: pass - case case: pass + case something: + pass match match: @@ -98,10 +82,8 @@ def func(match: case, case: match) -> case: match something: case 1 as a: pass - case 2 as b, 3 as c: pass - case 4 as d, (5 as e), (6 | 7 as g), *h: pass diff --git a/tests/data/cases/preview_pattern_matching_long.py b/tests/data/cases/preview_pattern_matching_long.py new file mode 100644 index 00000000000..df849fdc4f2 --- /dev/null +++ b/tests/data/cases/preview_pattern_matching_long.py @@ -0,0 +1,34 @@ +# flags: --preview --minimum-version=3.10 +match x: + case "abcd" | "abcd" | "abcd" : + pass + case "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd": + pass + case xxxxxxxxxxxxxxxxxxxxxxx: + pass + +# output + +match x: + case "abcd" | "abcd" | "abcd": + pass + case ( + "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + | "abcd" + ): + pass + case xxxxxxxxxxxxxxxxxxxxxxx: + pass diff --git a/tests/data/cases/preview_pattern_matching_trailing_comma.py b/tests/data/cases/preview_pattern_matching_trailing_comma.py new file mode 100644 index 00000000000..e6c0d88bb80 --- /dev/null +++ b/tests/data/cases/preview_pattern_matching_trailing_comma.py @@ -0,0 +1,39 @@ +# flags: --preview --minimum-version=3.10 +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +# output + +match maybe, multiple: + case perhaps, 5: + pass + case ( + perhaps, + 6, + ): + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case ( + [[5], (6)], + [7], + ): + pass + case _: + pass \ No newline at end of file From 66008fda5dc07f5626e5f5d0dcefc476a9c12ab8 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Tue, 7 Nov 2023 21:29:24 +0200 Subject: [PATCH 626/700] [563] Fix standalone comments inside complex blocks crashing Black (#4016) Bracket depth is not an accurate indicator of standalone comment position inside more complex blocks because bracket depth can be virtual (in loops' and lambdas' parameter blocks) or from optional parens. Here we try to stop cumulating lines upon standalone comments in complex blocks, and try to make standalone comment processing more simple. The fundamental idea is, that if we have a standalone comment, it needs to go on its own line, so we always have to split. This is not perfect, but at least a first step. --- CHANGES.md | 1 + src/black/brackets.py | 7 ++ src/black/linegen.py | 4 +- src/black/lines.py | 27 ++++- tests/data/cases/comments_in_blocks.py | 111 ++++++++++++++++++ ..._parens_with_braces_and_square_brackets.py | 20 ++++ 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 tests/data/cases/comments_in_blocks.py diff --git a/CHANGES.md b/CHANGES.md index c8ba83b5ae9..d30622b7786 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ - Fix crash on formatting bytes strings that look like docstrings (#4003) - Fix crash when whitespace followed a backslash before newline in a docstring (#4008) +- Fix standalone comments inside complex blocks crashing Black (#4016) - Fix crash on formatting code like `await (a ** b)` (#3994) diff --git a/src/black/brackets.py b/src/black/brackets.py index 85dac6edd1e..3020cc0d390 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -127,6 +127,13 @@ def mark(self, leaf: Leaf) -> None: self.maybe_increment_lambda_arguments(leaf) self.maybe_increment_for_loop_variable(leaf) + def any_open_for_or_lambda(self) -> bool: + """Return True if there is an open for or lambda expression on the line. + + See maybe_increment_for_loop_variable and maybe_increment_lambda_arguments + for details.""" + return bool(self._for_loop_depths or self._lambda_argument_depths) + def any_open_brackets(self) -> bool: """Return True if there is an yet unmatched open bracket on the line.""" return bool(self.bracket_match) diff --git a/src/black/linegen.py b/src/black/linegen.py index 30cfff3e846..e2c961d7a01 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -861,8 +861,6 @@ def _maybe_split_omitting_optional_parens( # it's not an import (optional parens are the only thing we can split on # in this case; attempting a split without them is a waste of time) and not line.is_import - # there are no standalone comments in the body - and not rhs.body.contains_standalone_comments(0) # and we can actually remove the parens and can_omit_invisible_parens(rhs, mode.line_length) ): @@ -1181,7 +1179,7 @@ def standalone_comment_split( line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: """Split standalone comments from the rest of the line.""" - if not line.contains_standalone_comments(0): + if not line.contains_standalone_comments(): raise CannotSplit("Line does not have any standalone comments") current_line = Line( diff --git a/src/black/lines.py b/src/black/lines.py index 23c1a93d3d4..f0cf25ba3e7 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1,6 +1,5 @@ import itertools import math -import sys from dataclasses import dataclass, field from typing import ( Callable, @@ -103,7 +102,10 @@ def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None: Raises ValueError when any `leaf` is appended after a standalone comment or when a standalone comment is not the first leaf on the line. """ - if self.bracket_tracker.depth == 0: + if ( + self.bracket_tracker.depth == 0 + or self.bracket_tracker.any_open_for_or_lambda() + ): if self.is_comment: raise ValueError("cannot append to standalone comments") @@ -233,10 +235,10 @@ def is_fmt_pass_converted( leaf.fmt_pass_converted_first_leaf ) - def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool: + def contains_standalone_comments(self) -> bool: """If so, needs to be split before emitting.""" for leaf in self.leaves: - if leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit: + if leaf.type == STANDALONE_COMMENT: return True return False @@ -982,6 +984,23 @@ def can_omit_invisible_parens( are too long. """ line = rhs.body + + # We need optional parens in order to split standalone comments to their own lines + # if there are no nested parens around the standalone comments + closing_bracket: Optional[Leaf] = None + for leaf in reversed(line.leaves): + if closing_bracket and leaf is closing_bracket.opening_bracket: + closing_bracket = None + if leaf.type == STANDALONE_COMMENT and not closing_bracket: + return False + if ( + not closing_bracket + and leaf.type in CLOSING_BRACKETS + and leaf.opening_bracket in line.leaves + and leaf.value + ): + closing_bracket = leaf + bt = line.bracket_tracker if not bt.delimiters: # Without delimiters the optional parentheses are useless. diff --git a/tests/data/cases/comments_in_blocks.py b/tests/data/cases/comments_in_blocks.py new file mode 100644 index 00000000000..1221139b6d8 --- /dev/null +++ b/tests/data/cases/comments_in_blocks.py @@ -0,0 +1,111 @@ +# Test cases from: +# - https://github.com/psf/black/issues/1798 +# - https://github.com/psf/black/issues/1499 +# - https://github.com/psf/black/issues/1211 +# - https://github.com/psf/black/issues/563 + +( + lambda + # a comment + : None +) + +( + lambda: + # b comment + None +) + +( + lambda + # a comment + : + # b comment + None +) + +[ + x + # Let's do this + for + # OK? + x + # Some comment + # And another + in + # One more + y +] + +return [ + (offers[offer_index], 1.0) + for offer_index, _ + # avoid returning any offers that don't match the grammar so + # that the return values here are consistent with what would be + # returned in AcceptValidHeader + in self._parse_and_normalize_offers(offers) +] + +from foo import ( + bar, + # qux +) + + +def convert(collection): + # replace all variables by integers + replacement_dict = { + variable: f"{index}" + for index, variable + # 0 is reserved as line terminator + in enumerate(collection.variables(), start=1) + } + + +{ + i: i + for i + # a comment + in range(5) +} + + +def get_subtree_proof_nodes( + chunk_index_groups: Sequence[Tuple[int, ...], ...], +) -> Tuple[int, ...]: + subtree_node_paths = ( + # We take a candidate element from each group and shift it to + # remove the bits that are not common to other group members, then + # we convert it to a tree path that all elements from this group + # have in common. + chunk_index + for chunk_index, bits_to_truncate + # Each group will contain an even "power-of-two" number of# elements. + # This tells us how many tailing bits each element has# which need to + # be truncated to get the group's common prefix. + in ((group[0], (len(group) - 1).bit_length()) for group in chunk_index_groups) + ) + return subtree_node_paths + + +if ( + # comment1 + a + # comment2 + or ( + # comment3 + ( + # comment4 + b + ) + # comment5 + and + # comment6 + c + or ( + # comment7 + d + ) + ) +): + print("Foo") diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 51fe516add5..97b5b2e8dd1 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -152,6 +152,16 @@ def foo_square_brackets(request): foo(**{x: y for x, y in enumerate(["long long long long line","long long long long line"])}) +for foo in ["a", "b"]: + output.extend([ + individual + for + # Foobar + container in xs_by_y[foo] + # Foobar + for individual in container["nested"] + ]) + # output def foo_brackets(request): return JsonResponse({ @@ -323,3 +333,13 @@ def foo_square_brackets(request): foo(**{ x: y for x, y in enumerate(["long long long long line", "long long long long line"]) }) + +for foo in ["a", "b"]: + output.extend([ + individual + for + # Foobar + container in xs_by_y[foo] + # Foobar + for individual in container["nested"] + ]) From 2e4fac9d87615e904a49e46a9cab2293e0b13126 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:31:44 -0800 Subject: [PATCH 627/700] Apply force exclude logic before symlink resolution (#4015) --- CHANGES.md | 2 +- src/black/__init__.py | 24 ++++++++++++++---------- tests/test_black.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d30622b7786..17882194aad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,7 +34,7 @@ ### Configuration - Add support for single-line format skip with other comments on the same line (#3959) - +- Consistently apply force exclusion logic before resolving symlinks (#4015) - Fix a bug in the matching of absolute path names in `--include` (#3976) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 5aca3316df0..2455e8648fc 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -682,7 +682,19 @@ def get_sources( path = Path(s) is_stdin = False + # Compare the logic here to the logic in `gen_python_files`. if is_stdin or path.is_file(): + root_relative_path = path.absolute().relative_to(root).as_posix() + + root_relative_path = "/" + root_relative_path + + # Hard-exclude any files that matches the `--force-exclude` regex. + if path_is_excluded(root_relative_path, force_exclude): + report.path_ignored( + path, "matches the --force-exclude regular expression" + ) + continue + normalized_path: Optional[str] = normalize_path_maybe_ignore( path, root, report ) @@ -690,16 +702,6 @@ def get_sources( if verbose: out(f'Skipping invalid source: "{normalized_path}"', fg="red") continue - if verbose: - out(f'Found input source: "{normalized_path}"', fg="blue") - - normalized_path = "/" + normalized_path - # Hard-exclude any files that matches the `--force-exclude` regex. - if path_is_excluded(normalized_path, force_exclude): - report.path_ignored( - path, "matches the --force-exclude regular expression" - ) - continue if is_stdin: path = Path(f"{STDIN_PLACEHOLDER}{str(path)}") @@ -709,6 +711,8 @@ def get_sources( ): continue + if verbose: + out(f'Found input source: "{normalized_path}"', fg="blue") sources.add(path) elif path.is_dir(): path = root / (path.resolve().relative_to(root)) diff --git a/tests/test_black.py b/tests/test_black.py index c9819742425..899cbeb111d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2637,6 +2637,23 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: stdin_filename=stdin_filename, ) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) + def test_get_sources_with_stdin_filename_and_force_exclude_and_symlink( + self, + ) -> None: + # Force exclude should exclude a symlink based on the symlink, not its target + path = THIS_DIR / "data" / "include_exclude_tests" + stdin_filename = str(path / "symlink.py") + expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"] + target = path / "b/exclude/a.py" + with patch("pathlib.Path.resolve", return_value=target): + assert_collected_sources( + src=["-"], + expected=expected, + force_exclude=r"exclude/a\.py", + stdin_filename=stdin_filename, + ) + class TestDeFactoAPI: """Test that certain symbols that are commonly used externally keep working. From f4c7be5445c87d9af5eba3d12faea62d2635e3d8 Mon Sep 17 00:00:00 2001 From: Abdenour Madani <61651582+Ab2nour@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:40:10 +0100 Subject: [PATCH 628/700] docs: fix minor typo (#4030) Replace "E950" with "B950" From 1a7d9c2f58de1ffcbbe6d133f60f283601ba3f54 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Wed, 8 Nov 2023 06:19:32 +0200 Subject: [PATCH 629/700] Preserve visible quote types for f-string debug expressions (#4005) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + src/black/trans.py | 21 +++++-- tests/data/cases/preview_long_strings.py | 80 ++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 17882194aad..26e4db5848b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ indented less (#3964) - Multiline list and dict unpacking as the sole argument to a function is now also indented less (#3992) +- In f-string debug expressions preserve quote types that are visible in the final + string (#4005) - Fix a bug where long `case` blocks were not split into multiple lines. Also enable general trailing comma rules on `case` blocks (#4024) - Keep requiring two empty lines between module-level docstring and first function or diff --git a/src/black/trans.py b/src/black/trans.py index a3f6467cc9e..ab3197fa6df 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -590,11 +590,22 @@ def make_naked(string: str, string_prefix: str) -> str: """ assert_is_leaf_string(string) if "f" in string_prefix: - string = _toggle_fexpr_quotes(string, QUOTE) - # After quotes toggling, quotes in expressions won't be escaped - # because quotes can't be reused in f-strings. So we can simply - # let the escaping logic below run without knowing f-string - # expressions. + f_expressions = ( + string[span[0] + 1 : span[1] - 1] # +-1 to get rid of curly braces + for span in iter_fexpr_spans(string) + ) + debug_expressions_contain_visible_quotes = any( + re.search(r".*[\'\"].*(? Date: Wed, 8 Nov 2023 06:21:33 +0200 Subject: [PATCH 630/700] Remove redundant condition from `has_magic_trailing_comma` (#4023) The second `if` cannot be true at its execution point, because it is already covered by the first `if`. The condition `comma.parent.type == syms.subscriptlist` always holds if `closing.parent.type == syms.trailer` holds, because `subscriptlist` only appears inside `trailer` in the grammar: ``` trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: (subscript|star_expr) (',' (subscript|star_expr))* [','] ``` --- src/black/lines.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index f0cf25ba3e7..3ade0a5f4a5 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -353,9 +353,9 @@ def has_magic_trailing_comma( if closing.type == token.RSQB: if ( - closing.parent + closing.parent is not None and closing.parent.type == syms.trailer - and closing.opening_bracket + and closing.opening_bracket is not None and is_one_sequence_between( closing.opening_bracket, closing, @@ -365,22 +365,7 @@ def has_magic_trailing_comma( ): return False - if not ensure_removable: - return True - - comma = self.leaves[-1] - if comma.parent is None: - return False - return ( - comma.parent.type != syms.subscriptlist - or closing.opening_bracket is None - or not is_one_sequence_between( - closing.opening_bracket, - closing, - self.leaves, - brackets=(token.LSQB, token.RSQB), - ) - ) + return True if self.is_import: return True From 2a1c67e0b2f81df602ec1f6e7aeb030b9709dc7c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 20:44:46 -0800 Subject: [PATCH 631/700] Prepare release 23.11.0 (#4032) --- CHANGES.md | 70 +++------------------ docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +- 3 files changed, 14 insertions(+), 66 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 26e4db5848b..b565d510a71 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,78 +1,49 @@ # Change Log -## Unreleased +## 23.11.0 ### Highlights - - - Support formatting ranges of lines with the new `--line-ranges` command-line option - (#4020). + (#4020) ### Stable style - Fix crash on formatting bytes strings that look like docstrings (#4003) - Fix crash when whitespace followed a backslash before newline in a docstring (#4008) - Fix standalone comments inside complex blocks crashing Black (#4016) - - Fix crash on formatting code like `await (a ** b)` (#3994) - - No longer treat leading f-strings as docstrings. This matches Python's behaviour and fixes a crash (#4019) ### Preview style -- Multiline dictionaries and lists that are the sole argument to a function are now - indented less (#3964) -- Multiline list and dict unpacking as the sole argument to a function is now also +- Multiline dicts and lists that are the sole argument to a function are now indented + less (#3964) +- Multiline unpacked dicts and lists as the sole argument to a function are now also indented less (#3992) -- In f-string debug expressions preserve quote types that are visible in the final - string (#4005) +- In f-string debug expressions, quote types that are visible in the final string are + now preserved (#4005) - Fix a bug where long `case` blocks were not split into multiple lines. Also enable general trailing comma rules on `case` blocks (#4024) - Keep requiring two empty lines between module-level docstring and first function or - class definition. (#4028) + class definition (#4028) +- Add support for single-line format skip with other comments on the same line (#3959) ### Configuration -- Add support for single-line format skip with other comments on the same line (#3959) - Consistently apply force exclusion logic before resolving symlinks (#4015) - Fix a bug in the matching of absolute path names in `--include` (#3976) -### Packaging - - - -### Parser - - - ### Performance - - - Fix mypyc builds on arm64 on macOS (#4017) -### Output - - - -### _Blackd_ - - - ### Integrations - - - Black's pre-commit integration will now run only on git hooks appropriate for a code formatter (#3940) -### Documentation - - - ## 23.10.1 ### Highlights @@ -327,8 +298,6 @@ versions separately. ### Stable style - - - Introduce the 2023 stable style, which incorporates most aspects of last year's preview style (#3418). Specific changes: - Enforce empty lines before classes and functions with sticky leading comments @@ -362,8 +331,6 @@ versions separately. ### Preview style - - - Format hex codes in unicode escape sequences in string literals (#2916) - Add parentheses around `if`-`else` expressions (#2278) - Improve performance on large expressions that contain many strings (#3467) @@ -394,15 +361,11 @@ versions separately. ### Configuration - - - Black now tries to infer its `--target-version` from the project metadata specified in `pyproject.toml` (#3219) ### Packaging - - - Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7 (#3380) - This also fixes some crashes while using compiled Black with a debug build of @@ -415,8 +378,6 @@ versions separately. ### Output - - - Calling `black --help` multiple times will return the same help contents each time (#3516) - Verbose logging now shows the values of `pyproject.toml` configuration variables @@ -426,25 +387,18 @@ versions separately. ### Integrations - - - Move 3.11 CI to normal flow now that all dependencies support 3.11 (#3446) - Docker: Add new `latest_prerelease` tag automation to follow latest black alpha release on docker images (#3465) ### Documentation - - - Expand `vim-plug` installation instructions to offer more explicit options (#3468) ## 22.12.0 ### Preview style - - - Enforce empty lines before classes and functions with sticky leading comments (#3302) - Reformat empty and whitespace-only files as either an empty file (if no newline is present) or as a single newline character (if a newline is present) (#3348) @@ -457,8 +411,6 @@ versions separately. ### Configuration - - - Fix incorrectly applied `.gitignore` rules by considering the `.gitignore` location and the relative path to the target file (#3338) - Fix incorrectly ignoring `.gitignore` presence when more than one source directory is @@ -466,16 +418,12 @@ versions separately. ### Parser - - - Parsing support has been added for walruses inside generator expression that are passed as function args (for example, `any(match := my_re.match(text) for text in texts)`) (#3327). ### Integrations - - - Vim plugin: Optionally allow using the system installation of Black via `let g:black_use_virtualenv = 0`(#3309) diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 597a6b993c7..3c7ef89918f 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black-jupyter # 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 dbd8c7ba434..6e7ee584cf9 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -211,8 +211,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.10.1 (compiled: yes) -$ black --required-version 23.10.1 -c "format = 'this'" +black, 23.11.0 (compiled: yes) +$ black --required-version 23.11.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -303,7 +303,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.10.1 +black, 23.11.0 ``` #### `--config` From 58f31a70efe6509ce8213afac998bc5d5bb7e34d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 22:10:35 -0800 Subject: [PATCH 632/700] Add new release template --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b565d510a71..9446927b8d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.11.0 ### Highlights From 1b6b0bfcac37428f7f2eb6c97fd0a25628324db7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 17 Nov 2023 15:57:00 +0000 Subject: [PATCH 633/700] Improve annotations for `black.concurrency.cancel` (#4047) --- src/black/concurrency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 55c96b66c86..ff0a8f5fd32 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -38,7 +38,7 @@ def maybe_install_uvloop() -> None: pass -def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: +def cancel(tasks: Iterable["asyncio.Future[Any]"]) -> None: """asyncio signal handler that cancels all `tasks` and reports to stderr.""" err("Aborted!") for task in tasks: From 5773d5cd2b532da185808f974a5875ca09064e28 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:39:44 -0600 Subject: [PATCH 634/700] Document target version inference (#4048) --- docs/usage_and_configuration/the_basics.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 6e7ee584cf9..546fdc474e8 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -67,6 +67,10 @@ In a [configuration file](#configuration-via-a-file), you can write: target-version = ["py38", "py39", "py310", "py311"] ``` +By default, Black will infer target versions from the project metadata in +`pyproject.toml`, specifically the `[project.requires-python]` field. If this does not +yield conclusive results, Black will use per-file auto-detection. + _Black_ uses this option to decide what grammar to use to parse your code. In addition, it may use it to decide what style to use. For example, support for a trailing comma after `*args` in a function call was added in Python 3.5, so _Black_ will add this comma From 85b1c71a3445f32860d7b139ae4de4824f6ae102 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:15:07 +0000 Subject: [PATCH 635/700] Block aiohttp==3.9.0 from being installed in CI on Windows/pypy (#4051) --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c0302d2302a..bea8e77ba04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,8 @@ dynamic = ["readme", "version"] colorama = ["colorama>=0.4.3"] uvloop = ["uvloop>=0.15.2"] d = [ - "aiohttp>=3.7.4", + "aiohttp>=3.7.4; sys_platform != 'win32' or implementation_name != 'pypy'", + "aiohttp>=3.7.4, !=3.9.0; sys_platform == 'win32' and implementation_name == 'pypy'", ] jupyter = [ "ipython>=7.8.0", From c4cd200a063a4d5546b547809aa1e607f03c3f59 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:41:46 +0000 Subject: [PATCH 636/700] Make flake8 pass when run with Python 3.12 (#4050) --- src/black/parsing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/parsing.py b/src/black/parsing.py index ea282d1805c..178a7ef10e2 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -175,7 +175,7 @@ def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: except AttributeError: continue - yield f"{' ' * (depth+1)}{field}=" + yield f"{' ' * (depth + 1)}{field}=" if isinstance(value, list): for item in value: @@ -211,6 +211,6 @@ def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: normalized = value.rstrip() else: normalized = value - yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" + yield f"{' ' * (depth + 2)}{normalized!r}, # {value.__class__.__name__}" yield f"{' ' * depth}) # /{node.__class__.__name__}" From d93a942a79762484a0f72c6fa271b45ec377009b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:42:36 +0000 Subject: [PATCH 637/700] Upgrade mypy to 1.6.1 (#4049) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 2 +- pyproject.toml | 4 ++-- scripts/check_pre_commit_rev_in_example.py | 2 +- scripts/check_version_in_basics_example.py | 2 +- scripts/diff_shades_gha_helper.py | 2 +- scripts/fuzz.py | 2 +- scripts/make_width_table.py | 2 +- src/blackd/__init__.py | 4 +--- tests/optional.py | 10 +++++++++- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 623e661ac07..c153746b621 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.1 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index 9446927b8d1..13b6c7bdb21 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ ### Packaging - +- Upgrade to mypy 1.6.1 (#4049) ### Parser diff --git a/pyproject.toml b/pyproject.toml index bea8e77ba04..f8f5155e898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ macos-max-compat = true enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.5.1", + "mypy==1.6.1", "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true @@ -187,7 +187,7 @@ CC = "clang" build-frontend = { name = "build", args = ["--no-isolation"] } # Unfortunately, hatch doesn't respect MACOSX_DEPLOYMENT_TARGET before-build = [ - "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.5.1' 'click==8.1.3'", + "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.6.1' 'click==8.1.3'", """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, ] diff --git a/scripts/check_pre_commit_rev_in_example.py b/scripts/check_pre_commit_rev_in_example.py index 107c6444dca..cc45a31e1ed 100644 --- a/scripts/check_pre_commit_rev_in_example.py +++ b/scripts/check_pre_commit_rev_in_example.py @@ -14,7 +14,7 @@ import commonmark import yaml -from bs4 import BeautifulSoup # type: ignore[import] +from bs4 import BeautifulSoup # type: ignore[import-untyped] def main(changes: str, source_version_control: str) -> None: diff --git a/scripts/check_version_in_basics_example.py b/scripts/check_version_in_basics_example.py index 0f42bafe334..c90fdb43da5 100644 --- a/scripts/check_version_in_basics_example.py +++ b/scripts/check_version_in_basics_example.py @@ -8,7 +8,7 @@ import sys import commonmark -from bs4 import BeautifulSoup # type: ignore[import] +from bs4 import BeautifulSoup # type: ignore[import-untyped] def main(changes: str, the_basics: str) -> None: diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 895516deb51..8cd8ba155ce 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -119,7 +119,7 @@ def main() -> None: @main.command("config", help="Acquire run configuration and metadata.") @click.argument("event", type=click.Choice(["push", "pull_request"])) def config(event: Literal["push", "pull_request"]) -> None: - import diff_shades # type: ignore[import] + import diff_shades # type: ignore[import-not-found] if event == "push": jobs = [{"mode": "preview-changes", "force-flag": "--force-preview-style"}] diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 929d3eac4f5..018b66e0ea2 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -80,7 +80,7 @@ def test_idempotent_any_syntatically_valid_python( try: import sys - import atheris # type: ignore[import] + import atheris # type: ignore[import-not-found] except ImportError: pass else: diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 3c7cae60f7f..1b53c1b2cc9 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -20,7 +20,7 @@ from os.path import basename, dirname, join from typing import Iterable, Tuple -import wcwidth # type: ignore[import] +import wcwidth # type: ignore[import-not-found] def make_width_table() -> Iterable[Tuple[int, int, int]]: diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 972f24181cb..6b0f3d33295 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -74,9 +74,7 @@ def main(bind_host: str, bind_port: int) -> None: app = make_app() ver = black.__version__ black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}") - # TODO: aiohttp had an incorrect annotation for `print` argument, - # It'll be fixed once aiohttp releases that code - web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) # type: ignore[arg-type] + web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) def make_app() -> web.Application: diff --git a/tests/optional.py b/tests/optional.py index 3f5277b6b03..70ee823e316 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -26,7 +26,15 @@ from pytest import StashKey except ImportError: # pytest < 7 - from _pytest.store import StoreKey as StashKey # type: ignore[import, no-redef] + # + # "isort: skip" is needed or it moves the "type: ignore" to the following line + # because of the line length, and then mypy complains. + # Of course, adding the "isort: skip" means that + # flake8-bugbear then also complains about the line length, + # so we *also* need a "noqa" comment for good measure :) + from _pytest.store import ( # type: ignore[import-not-found, no-redef] # isort: skip # noqa: B950 + StoreKey as StashKey, + ) log = logging.getLogger(__name__) From 11da02da72ed437facde3658bb61ddebce21e7a4 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Sat, 18 Nov 2023 11:47:05 -0800 Subject: [PATCH 638/700] Handle more huggable immediately nested parens/brackets. (#4012) Fixes #4011 --- CHANGES.md | 3 + docs/conf.py | 36 +++--- docs/the_black_code_style/future_style.md | 17 ++- src/black/cache.py | 6 +- src/black/handle_ipynb_magics.py | 54 ++++---- src/black/linegen.py | 69 ++++++++-- ..._parens_with_braces_and_square_brackets.py | 122 ++++++++++++++++-- .../cases/preview_long_strings__regression.py | 15 +-- 8 files changed, 233 insertions(+), 89 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 13b6c7bdb21..29f037b4767 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Additional cases of immediately nested tuples, lists, and dictionaries are now + indented less (#4012) + ### Configuration diff --git a/docs/conf.py b/docs/conf.py index 6b645435325..52a849d06a4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -149,15 +149,13 @@ def make_pypi_svg(version: str) -> None: # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "black.tex", - "Documentation for Black", - "Łukasz Langa and contributors to Black", - "manual", - ) -] +latex_documents = [( + master_doc, + "black.tex", + "Documentation for Black", + "Łukasz Langa and contributors to Black", + "manual", +)] # -- Options for manual page output ------------------------------------------ @@ -172,17 +170,15 @@ def make_pypi_svg(version: str) -> None: # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "Black", - "Documentation for Black", - author, - "Black", - "The uncompromising Python code formatter", - "Miscellaneous", - ) -] +texinfo_documents = [( + master_doc, + "Black", + "Documentation for Black", + author, + "Black", + "The uncompromising Python code formatter", + "Miscellaneous", +)] # -- Options for Epub output ------------------------------------------------- diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 944ffad033e..428bd87ab50 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -116,8 +116,7 @@ my_dict = { ### Improved multiline dictionary and list indentation for sole function parameter For better readability and less verticality, _Black_ now pairs parentheses ("(", ")") -with braces ("{", "}") and square brackets ("[", "]") on the same line for single -parameter function calls. For example: +with braces ("{", "}") and square brackets ("[", "]") on the same line. For example: ```python foo( @@ -127,6 +126,14 @@ foo( 3, ] ) + +nested_array = [ + [ + 1, + 2, + 3, + ] +] ``` will be changed to: @@ -137,6 +144,12 @@ foo([ 2, 3, ]) + +nested_array = [[ + 1, + 2, + 3, +]] ``` This also applies to list and dictionary unpacking: diff --git a/src/black/cache.py b/src/black/cache.py index 6a332304981..6baa096baca 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -124,9 +124,9 @@ def filtered_cached(self, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path] def write(self, sources: Iterable[Path]) -> None: """Update the cache file data and write a new cache file.""" - self.file_data.update(**{ - str(src.resolve()): Cache.get_file_data(src) for src in sources - }) + self.file_data.update( + **{str(src.resolve()): Cache.get_file_data(src) for src in sources} + ) try: CACHE_DIR.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 55ef2267df8..5b2847cb0c4 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -17,36 +17,30 @@ from black.output import out from black.report import NothingChanged -TRANSFORMED_MAGICS = frozenset( - ( - "get_ipython().run_cell_magic", - "get_ipython().system", - "get_ipython().getoutput", - "get_ipython().run_line_magic", - ) -) -TOKENS_TO_IGNORE = frozenset( - ( - "ENDMARKER", - "NL", - "NEWLINE", - "COMMENT", - "DEDENT", - "UNIMPORTANT_WS", - "ESCAPED_NL", - ) -) -PYTHON_CELL_MAGICS = frozenset( - ( - "capture", - "prun", - "pypy", - "python", - "python3", - "time", - "timeit", - ) -) +TRANSFORMED_MAGICS = frozenset(( + "get_ipython().run_cell_magic", + "get_ipython().system", + "get_ipython().getoutput", + "get_ipython().run_line_magic", +)) +TOKENS_TO_IGNORE = frozenset(( + "ENDMARKER", + "NL", + "NEWLINE", + "COMMENT", + "DEDENT", + "UNIMPORTANT_WS", + "ESCAPED_NL", +)) +PYTHON_CELL_MAGICS = frozenset(( + "capture", + "prun", + "pypy", + "python", + "python3", + "time", + "timeit", +)) TOKEN_HEX = secrets.token_hex diff --git a/src/black/linegen.py b/src/black/linegen.py index e2c961d7a01..8a2cd4710b9 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -817,25 +817,66 @@ def _first_right_hand_split( body_leaves.reverse() head_leaves.reverse() - if Preview.hug_parens_with_braces_and_square_brackets in line.mode: - is_unpacking = 1 if body_leaves[0].type in [token.STAR, token.DOUBLESTAR] else 0 - if ( - tail_leaves[0].type == token.RPAR - and tail_leaves[0].value - and tail_leaves[0].opening_bracket is head_leaves[-1] - and body_leaves[-1].type in [token.RBRACE, token.RSQB] - and body_leaves[-1].opening_bracket is body_leaves[is_unpacking] + body: Optional[Line] = None + if ( + Preview.hug_parens_with_braces_and_square_brackets in line.mode + and tail_leaves[0].value + and tail_leaves[0].opening_bracket is head_leaves[-1] + ): + inner_body_leaves = list(body_leaves) + hugged_opening_leaves: List[Leaf] = [] + hugged_closing_leaves: List[Leaf] = [] + is_unpacking = body_leaves[0].type in [token.STAR, token.DOUBLESTAR] + unpacking_offset: int = 1 if is_unpacking else 0 + while ( + len(inner_body_leaves) >= 2 + unpacking_offset + and inner_body_leaves[-1].type in CLOSING_BRACKETS + and inner_body_leaves[-1].opening_bracket + is inner_body_leaves[unpacking_offset] ): - head_leaves = head_leaves + body_leaves[: 1 + is_unpacking] - tail_leaves = body_leaves[-1:] + tail_leaves - body_leaves = body_leaves[1 + is_unpacking : -1] + if unpacking_offset: + hugged_opening_leaves.append(inner_body_leaves.pop(0)) + unpacking_offset = 0 + hugged_opening_leaves.append(inner_body_leaves.pop(0)) + hugged_closing_leaves.insert(0, inner_body_leaves.pop()) + + if hugged_opening_leaves and inner_body_leaves: + inner_body = bracket_split_build_line( + inner_body_leaves, + line, + hugged_opening_leaves[-1], + component=_BracketSplitComponent.body, + ) + if ( + line.mode.magic_trailing_comma + and inner_body_leaves[-1].type == token.COMMA + ): + should_hug = True + else: + line_length = line.mode.line_length - sum( + len(str(leaf)) + for leaf in hugged_opening_leaves + hugged_closing_leaves + ) + if is_line_short_enough( + inner_body, mode=replace(line.mode, line_length=line_length) + ): + # Do not hug if it fits on a single line. + should_hug = False + else: + should_hug = True + if should_hug: + body_leaves = inner_body_leaves + head_leaves.extend(hugged_opening_leaves) + tail_leaves = hugged_closing_leaves + tail_leaves + body = inner_body # No need to re-calculate the body again later. head = bracket_split_build_line( head_leaves, line, opening_bracket, component=_BracketSplitComponent.head ) - body = bracket_split_build_line( - body_leaves, line, opening_bracket, component=_BracketSplitComponent.body - ) + if body is None: + body = bracket_split_build_line( + body_leaves, line, opening_bracket, component=_BracketSplitComponent.body + ) tail = bracket_split_build_line( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 97b5b2e8dd1..9e5c9eb8546 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -128,6 +128,19 @@ def foo_square_brackets(request): func({"short line"}) func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) +func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) +func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) +func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) + +# Do not hug if the argument fits on a single line. +func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) +func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) +func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) +func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) +func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) +array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] +array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] +array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} @@ -137,6 +150,13 @@ def foo_square_brackets(request): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) +nested_mapping = {"key": [{"a very long key 1": "with a very long value", "a very long key 2": "with a very long value"}]} +nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] +explicit_exploding = [[["short", "line",],],] +single_item_do_not_explode = Context({ + "version": get_docs_version(), +}) + foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) @@ -152,6 +172,9 @@ def foo_square_brackets(request): foo(**{x: y for x, y in enumerate(["long long long long line","long long long long line"])}) +# Edge case when deciding whether to hug the brackets without inner content. +very_very_very_long_variable = very_very_very_long_module.VeryVeryVeryVeryLongClassName([[]]) + for foo in ["a", "b"]: output.extend([ individual @@ -276,9 +299,9 @@ def foo_square_brackets(request): ) func([x for x in "short line"]) -func([ - x for x in "long line long line long line long line long line long line long line" -]) +func( + [x for x in "long line long line long line long line long line long line long line"] +) func([ x for x in [ @@ -295,15 +318,60 @@ def foo_square_brackets(request): "long long long long line", "long long long long long line", }) -func({ - { - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", - } -}) +func({{ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}}) +func(( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +)) +func((( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +))) +func([[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]) + +# Do not hug if the argument fits on a single line. +func( + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +) +func( + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +) +func( + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +) +func( + **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} +) +func( + *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") +) +array = [ + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +] +array = [ + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +] +array = [ + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +] foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} @@ -313,6 +381,31 @@ def foo_square_brackets(request): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) +nested_mapping = { + "key": [{ + "a very long key 1": "with a very long value", + "a very long key 2": "with a very long value", + }] +} +nested_array = [[[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]] +explicit_exploding = [ + [ + [ + "short", + "line", + ], + ], +] +single_item_do_not_explode = Context({ + "version": get_docs_version(), +}) + foo(*[ "long long long long long line", "long long long long long line", @@ -334,6 +427,11 @@ def foo_square_brackets(request): x: y for x, y in enumerate(["long long long long line", "long long long long line"]) }) +# Edge case when deciding whether to hug the brackets without inner content. +very_very_very_long_variable = very_very_very_long_module.VeryVeryVeryVeryLongClassName( + [[]] +) + for foo in ["a", "b"]: output.extend([ individual diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 313d898cd83..5e76a8cf61c 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -611,14 +611,13 @@ def foo(): class A: def foo(): - XXXXXXXXXXXX.append( - ( - "xxx_xxxxxxxxxx(xxxxx={}, xxxx={}, xxxxx, xxxx_xxxx_xxxxxxxxxx={})" - .format(xxxxx, xxxx, xxxx_xxxx_xxxxxxxxxx), - my_var, - my_other_var, - ) - ) + XXXXXXXXXXXX.append(( + "xxx_xxxxxxxxxx(xxxxx={}, xxxx={}, xxxxx, xxxx_xxxx_xxxxxxxxxx={})".format( + xxxxx, xxxx, xxxx_xxxx_xxxxxxxxxx + ), + my_var, + my_other_var, + )) class A: From 80a166f2e115bda9f33d29a5ea313be2557dc7fd Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 12:09:55 -0800 Subject: [PATCH 639/700] Make black[d] install + test run with 3.12 (#4035) * Make black[d] install + test run with 3.12 - With aiohttp >= 3.9.0 we can now install all dependencies with 3.12 - Add actions to run 3.12 - Lint still needs to be 3.11 Test: - `python3.12 -m venv /tmp/tb --upgrade-deps` - `/tmp/tb/bin/pip install tox` - `/tmp/tb/bin/pip install .[d]` - `/tmp/tb/bin/tox -e py312` ``` py312: OK (37.61=setup[3.98]+cmd[3.83,0.36,19.54,6.46,3.00,0.44] seconds) congratulations :) (37.63 seconds) ``` * Move to pypy-3.9 --------- Co-authored-by: Jelle Zijlstra --- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/test.yml | 2 +- CHANGES.md | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 9a23e19cadd..fa3d87c70f5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "*" - name: Install dependencies run: | diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 1b5a50c0e0b..48c26452c54 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f33f2b814f..3f8928cc42a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index 29f037b4767..9f7ab685afe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,7 +43,7 @@ ### Integrations - +- Enable 3.12 CI (#4035) ### Documentation diff --git a/pyproject.toml b/pyproject.toml index f8f5155e898..e63e0aea3ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ [tool.black] line-length = 88 -target-version = ['py37', 'py38'] +target-version = ['py38'] include = '\.pyi?$' extend-exclude = ''' /( From 96faa3b469298573be9d3e2d55982328ee5feef9 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 18:09:47 -0800 Subject: [PATCH 640/700] [docker] Build with 3.12 image (#4055) Test: ``` crl-m1:black cooper$ docker build --tag black_3_12 . ... => [stage-1 2/2] COPY --from=builder /opt/venv /opt/venv 0.2s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:bd66acc9d76d2c40d287b0684ce6601401631e0468204c4e6a81f8f1eebaf1dd 0.0s => => naming to docker.io/library/black_3_12 crl-m1:black cooper$ docker image ls | grep black_3_12 black_3_12 latest bd66acc9d76d 59 seconds ago 193MB ``` --- CHANGES.md | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9f7ab685afe..b3beefdd80e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### Integrations - Enable 3.12 CI (#4035) +- Build docker images with 3.12 (#4055) ### Documentation diff --git a/Dockerfile b/Dockerfile index bfd9acccb99..ab961a2f491 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim AS builder +FROM python:3.12-slim AS builder RUN mkdir /src COPY . /src/ @@ -12,7 +12,7 @@ RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setupto && cd /src && hatch build -t wheel \ && pip install --no-cache-dir dist/*-cp*[colorama,d,uvloop] -FROM python:3.11-slim +FROM python:3.12-slim # copy only Python packages to limit the image size COPY --from=builder /opt/venv /opt/venv From f23b845a295f1422a1989f5ea1560ad2e0fadcdd Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 18:11:50 -0800 Subject: [PATCH 641/700] [ci] Move 'lint' to 3.12 (#4053) - Add to run on MacOS + Windows too - Do not install [d] dependecies as blackd is not actually run / checked - Move to default GitHub action version - which is 3.12 today --- .github/workflows/lint.yml | 12 ++++++++---- tox.ini | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7fe1b04eb02..d1ad23bb2ab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: Lint + format ourselves on: [push, pull_request] @@ -11,7 +11,11 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 @@ -26,12 +30,12 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "*" - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e '.[d]' + python -m pip install -e '.' python -m pip install tox - name: Run pre-commit hooks diff --git a/tox.ini b/tox.ini index 018cef993c0..8b4175d23fe 100644 --- a/tox.ini +++ b/tox.ini @@ -94,5 +94,5 @@ commands = setenv = PYTHONPATH = {toxinidir}/src skip_install = True commands = - pip install -e .[d] + pip install -e . black --check {toxinidir}/src {toxinidir}/tests From 30c6bb3651634ebf66177c85b27e7e277316eab7 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 19 Nov 2023 10:44:00 -0800 Subject: [PATCH 642/700] [docker ci] Split up amd64 (x86_64) and arm64 builds (#4054) * [docker ci] Split up amd64 (x86_64) and arm64 builds - Lets run them seperately to cut down total time - Will also more clearly show if either arch has specific problems - Kept amd64 (x86_64) using qemu actions so if GitHub ever offers arm64 boxes it could stay working too Fixes #3971 * Add CHANGES entry --------- Co-authored-by: Jelle Zijlstra --- .../{docker.yml => docker_amd64.yml} | 8 +-- .github/workflows/docker_arm64.yml | 69 +++++++++++++++++++ CHANGES.md | 1 + 3 files changed, 74 insertions(+), 4 deletions(-) rename .github/workflows/{docker.yml => docker_amd64.yml} (92%) create mode 100644 .github/workflows/docker_arm64.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker_amd64.yml similarity index 92% rename from .github/workflows/docker.yml rename to .github/workflows/docker_amd64.yml index ee858236fcf..846cd8c74e3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker_amd64.yml @@ -1,4 +1,4 @@ -name: docker +name: docker amd64 (x86_64) on: push: @@ -39,7 +39,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} @@ -50,7 +50,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest_release @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest_prerelease diff --git a/.github/workflows/docker_arm64.yml b/.github/workflows/docker_arm64.yml new file mode 100644 index 00000000000..ddd554165af --- /dev/null +++ b/.github/workflows/docker_arm64.yml @@ -0,0 +1,69 @@ +name: docker arm64 + +on: + push: + branches: + - "main" + release: + types: [published] + +permissions: + contents: read + +jobs: + docker: + if: github.repository == 'psf/black' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Check + set version tag + run: + echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo + latest_non_release)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} + + - name: Build and push latest_release tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + !github.event.release.prerelease }} + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest_release + + - name: Build and push latest_prerelease tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + github.event.release.prerelease }} + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest_prerelease + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGES.md b/CHANGES.md index b3beefdd80e..8d0f10a2f3a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### Integrations - Enable 3.12 CI (#4035) +- Build docker images in parallel (#4054) - Build docker images with 3.12 (#4055) ### Documentation From ec4a1525ee7f0daf05a2ab709123fbb0fe69e4e2 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 19 Nov 2023 11:28:00 -0800 Subject: [PATCH 643/700] [docker ci] Revert "parallel" builds in seperate actions (#4057) - Broke tagging images together - Saved only a few mins - x86_64 build is fast, time is all spent on cross compile of arm64 - Also remove evil copy pasta ... which is nice Was worth an attempt. --- .../{docker_amd64.yml => docker.yml} | 8 +-- .github/workflows/docker_arm64.yml | 69 ------------------- 2 files changed, 4 insertions(+), 73 deletions(-) rename .github/workflows/{docker_amd64.yml => docker.yml} (92%) delete mode 100644 .github/workflows/docker_arm64.yml diff --git a/.github/workflows/docker_amd64.yml b/.github/workflows/docker.yml similarity index 92% rename from .github/workflows/docker_amd64.yml rename to .github/workflows/docker.yml index 846cd8c74e3..ee858236fcf 100644 --- a/.github/workflows/docker_amd64.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,4 @@ -name: docker amd64 (x86_64) +name: docker on: push: @@ -39,7 +39,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} @@ -50,7 +50,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest_release @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest_prerelease diff --git a/.github/workflows/docker_arm64.yml b/.github/workflows/docker_arm64.yml deleted file mode 100644 index ddd554165af..00000000000 --- a/.github/workflows/docker_arm64.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: docker arm64 - -on: - push: - branches: - - "main" - release: - types: [published] - -permissions: - contents: read - -jobs: - docker: - if: github.repository == 'psf/black' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Check + set version tag - run: - echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo - latest_non_release)" >> $GITHUB_ENV - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - - - name: Build and push latest_release tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - !github.event.release.prerelease }} - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest_release - - - name: Build and push latest_prerelease tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - github.event.release.prerelease }} - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest_prerelease - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} From 89e28ea66f50d4281cb9f624e31566aed9d5aab1 Mon Sep 17 00:00:00 2001 From: tungol Date: Mon, 20 Nov 2023 20:44:33 -0800 Subject: [PATCH 644/700] Permit standalone form feed characters at the module level (#4021) Co-authored-by: Stephen Morton Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 +- .../reference/reference_functions.rst | 4 +- docs/the_black_code_style/future_style.md | 11 + src/black/comments.py | 39 ++- src/black/linegen.py | 25 +- src/black/lines.py | 14 +- src/black/mode.py | 1 + src/black/nodes.py | 7 + src/black/output.py | 23 +- src/blib2to3/pgen2/driver.py | 2 + tests/data/cases/preview_form_feeds.py | 225 ++++++++++++++++++ 11 files changed, 318 insertions(+), 35 deletions(-) create mode 100644 tests/data/cases/preview_form_feeds.py diff --git a/CHANGES.md b/CHANGES.md index 8d0f10a2f3a..4c3fbf1afc8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ ### Preview style - +- Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index dd92e37a7d4..ebadf6975a7 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -149,7 +149,7 @@ Utilities .. autofunction:: black.numerics.normalize_numeric_literal -.. autofunction:: black.linegen.normalize_prefix +.. autofunction:: black.comments.normalize_trailing_prefix .. autofunction:: black.strings.normalize_string_prefix @@ -168,3 +168,5 @@ Utilities .. autofunction:: black.strings.sub_twice .. autofunction:: black.nodes.whitespace + +.. autofunction:: black.nodes.make_simple_prefix diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 428bd87ab50..f55ea5f60a9 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -296,3 +296,14 @@ s = ( # Top comment # Bottom comment ) ``` + +======= + +### Form feed characters + +_Black_ will now retain form feed characters on otherwise empty lines at the module +level. Only one form feed is retained for a group of consecutive empty lines. Where +there are two empty lines in a row, the form feed will be placed on the second line. + +_Black_ already retained form feed literals inside a comment or inside a string. This +remains the case. diff --git a/src/black/comments.py b/src/black/comments.py index 862fc7607cc..8a0e925fdc0 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -10,6 +10,7 @@ WHITESPACE, container_of, first_leaf_of, + make_simple_prefix, preceding_leaf, syms, ) @@ -44,6 +45,7 @@ class ProtoComment: value: str # content of the comment newlines: int # how many newlines before the comment consumed: int # how many characters of the original leaf's prefix did we consume + form_feed: bool # is there a form feed before the comment def generate_comments(leaf: LN) -> Iterator[Leaf]: @@ -65,8 +67,12 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]: Inline comments are emitted as regular token.COMMENT leaves. Standalone are emitted with a fake STANDALONE_COMMENT token identifier. """ + total_consumed = 0 for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER): - yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) + total_consumed = pc.consumed + prefix = make_simple_prefix(pc.newlines, pc.form_feed) + yield Leaf(pc.type, pc.value, prefix=prefix) + normalize_trailing_prefix(leaf, total_consumed) @lru_cache(maxsize=4096) @@ -79,11 +85,14 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: consumed = 0 nlines = 0 ignored_lines = 0 - for index, line in enumerate(re.split("\r?\n", prefix)): - consumed += len(line) + 1 # adding the length of the split '\n' - line = line.lstrip() + form_feed = False + for index, full_line in enumerate(re.split("\r?\n", prefix)): + consumed += len(full_line) + 1 # adding the length of the split '\n' + line = full_line.lstrip() if not line: nlines += 1 + if "\f" in full_line: + form_feed = True if not line.startswith("#"): # Escaped newlines outside of a comment are not really newlines at # all. We treat a single-line comment following an escaped newline @@ -99,13 +108,33 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: comment = make_comment(line) result.append( ProtoComment( - type=comment_type, value=comment, newlines=nlines, consumed=consumed + type=comment_type, + value=comment, + newlines=nlines, + consumed=consumed, + form_feed=form_feed, ) ) + form_feed = False nlines = 0 return result +def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None: + """Normalize the prefix that's left over after generating comments. + + Note: don't use backslashes for formatting or you'll lose your voting rights. + """ + remainder = leaf.prefix[total_consumed:] + if "\\" not in remainder: + nl_count = remainder.count("\n") + form_feed = "\f" in remainder and remainder.endswith("\n") + leaf.prefix = make_simple_prefix(nl_count, form_feed) + return + + leaf.prefix = "" + + def make_comment(content: str) -> str: """Return a consistently formatted comment from the given `content` string. diff --git a/src/black/linegen.py b/src/black/linegen.py index 8a2cd4710b9..7fbbe290d7e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -149,7 +149,8 @@ def visit_default(self, node: LN) -> Iterator[Line]: self.current_line.append(comment) yield from self.line() - normalize_prefix(node, inside_brackets=any_open_brackets) + if any_open_brackets: + node.prefix = "" if self.mode.string_normalization and node.type == token.STRING: node.value = normalize_string_prefix(node.value) node.value = normalize_string_quotes(node.value) @@ -1035,8 +1036,6 @@ def bracket_split_build_line( result.inside_brackets = True result.depth += 1 if leaves: - # Since body is a new indent level, remove spurious leading whitespace. - normalize_prefix(leaves[0], inside_brackets=True) # Ensure a trailing comma for imports and standalone function arguments, but # be careful not to add one after any comments or within type annotations. no_commas = ( @@ -1106,7 +1105,7 @@ def split_wrapper( line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: for split_line in split_func(line, features, mode): - normalize_prefix(split_line.leaves[0], inside_brackets=True) + split_line.leaves[0].prefix = "" yield split_line return split_wrapper @@ -1250,24 +1249,6 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: yield current_line -def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: - """Leave existing extra newlines if not `inside_brackets`. Remove everything - else. - - Note: don't use backslashes for formatting or you'll lose your voting rights. - """ - if not inside_brackets: - spl = leaf.prefix.split("#") - if "\\" not in spl[0]: - nl_count = spl[-1].count("\n") - if len(spl) > 1: - nl_count -= 1 - leaf.prefix = "\n" * nl_count - return - - leaf.prefix = "" - - def normalize_invisible_parens( # noqa: C901 node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: diff --git a/src/black/lines.py b/src/black/lines.py index 3ade0a5f4a5..ec6145ff848 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -31,6 +31,7 @@ is_type_comment, is_type_ignore_comment, is_with_or_async_with_stmt, + make_simple_prefix, replace_child, syms, whitespace, @@ -520,12 +521,12 @@ class LinesBlock: before: int = 0 content_lines: List[str] = field(default_factory=list) after: int = 0 + form_feed: bool = False def all_lines(self) -> List[str]: empty_line = str(Line(mode=self.mode)) - return ( - [empty_line * self.before] + self.content_lines + [empty_line * self.after] - ) + prefix = make_simple_prefix(self.before, self.form_feed, empty_line) + return [prefix] + self.content_lines + [empty_line * self.after] @dataclass @@ -550,6 +551,12 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: This is for separating `def`, `async def` and `class` with extra empty lines (two on module-level). """ + form_feed = ( + Preview.allow_form_feeds in self.mode + and current_line.depth == 0 + and bool(current_line.leaves) + and "\f\n" in current_line.leaves[0].prefix + ) before, after = self._maybe_empty_lines(current_line) previous_after = self.previous_block.after if self.previous_block else 0 before = ( @@ -575,6 +582,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: original_line=current_line, before=before, after=after, + form_feed=form_feed, ) # Maintain the semantic_leading_comment state. diff --git a/src/black/mode.py b/src/black/mode.py index 1aa5cbecc86..04038f49627 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -194,6 +194,7 @@ class Preview(Enum): allow_empty_first_line_before_new_block_or_comment = auto() single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() + allow_form_feeds = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index 9251b0defb0..de53f8e36a3 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -407,6 +407,13 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no return SPACE +def make_simple_prefix(nl_count: int, form_feed: bool, empty_line: str = "\n") -> str: + """Generate a normalized prefix string.""" + if form_feed: + return (empty_line * (nl_count - 1)) + "\f" + empty_line + return empty_line * nl_count + + def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: """Return the first leaf that precedes `node`, if any.""" while node: diff --git a/src/black/output.py b/src/black/output.py index f4c17f28ea4..7c7dd0fe14e 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -4,8 +4,9 @@ """ import json +import re import tempfile -from typing import Any, Optional +from typing import Any, List, Optional from click import echo, style from mypy_extensions import mypyc_attr @@ -55,12 +56,28 @@ def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str: return "".join(diff_lines) +_line_pattern = re.compile(r"(.*?(?:\r\n|\n|\r|$))") + + +def _splitlines_no_ff(source: str) -> List[str]: + """Split a string into lines ignoring form feed and other chars. + + This mimics how the Python parser splits source code. + + A simplified version of the function with the same name in Lib/ast.py + """ + result = [match[0] for match in _line_pattern.finditer(source)] + if result[-1] == "": + result.pop(-1) + return result + + def diff(a: str, b: str, a_name: str, b_name: str) -> str: """Return a unified diff string between strings `a` and `b`.""" import difflib - a_lines = a.splitlines(keepends=True) - b_lines = b.splitlines(keepends=True) + a_lines = _splitlines_no_ff(a) + b_lines = _splitlines_no_ff(b) diff_lines = [] for line in difflib.unified_diff( a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index e629843f8b9..be3984437a8 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -222,6 +222,8 @@ def _partially_consume_prefix(self, prefix: str, column: int) -> Tuple[str, str] elif char == "\n": # unexpected empty line current_column = 0 + elif char == "\f": + current_column = 0 else: # indent is finished wait_for_nl = True diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py new file mode 100644 index 00000000000..2d8653a1f04 --- /dev/null +++ b/tests/data/cases/preview_form_feeds.py @@ -0,0 +1,225 @@ +# flags: --preview + + +# Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). +# These may be invisible in your editor: ensure you can see them before making changes here. + +# There's one at the start that'll get stripped + +# Comment and statement processing is different enough that we'll test variations of both +# contexts here + +# + + +# + + +# + + + +# + + + +# + + + +# + + +# + + + +# + +# + +# + + \ +# +pass + +pass + + +pass + + +pass + + + +pass + + + +pass + + + +pass + + +pass + + + +pass + +pass + +pass + + +# form feed after a dedent +def foo(): + pass + +pass + + +# form feeds are prohibited inside blocks, or on a line with nonwhitespace + def bar( a = 1 ,b : bool = False ) : + + + pass + + +class Baz: + + def __init__(self): + pass + + + def something(self): + pass + + + +# +pass +pass # +a = 1 + # + pass + a = 1 + +a = [ + +] + +# as internal whitespace of a comment is allowed but why +"form feed literal in a string is okay " + +# form feeds at the very end get removed. + + + +# output + +# Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). +# These may be invisible in your editor: ensure you can see them before making changes here. + +# There's one at the start that'll get stripped + +# Comment and statement processing is different enough that we'll test variations of both +# contexts here + +# + + +# + + +# + + +# + + +# + + +# + + +# + + +# + +# + +# + +# +pass + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + +pass + +pass + + +# form feed after a dedent +def foo(): + pass + + +pass + + +# form feeds are prohibited inside blocks, or on a line with nonwhitespace +def bar(a=1, b: bool = False): + pass + + +class Baz: + def __init__(self): + pass + + def something(self): + pass + + +# +pass +pass # +a = 1 +# +pass +a = 1 + +a = [] + +# as internal whitespace of a comment is allowed but why +"form feed literal in a string is okay " + +# form feeds at the very end get removed. From a8062983cd1f8ac8859297c870847906b10cf6a2 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Mon, 20 Nov 2023 20:45:39 -0800 Subject: [PATCH 645/700] Disable the stability check with --line-ranges for now. (#4034) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 ++ docs/usage_and_configuration/the_basics.md | 6 ++++ src/black/__init__.py | 9 ++++-- .../data/cases/line_ranges_diff_edge_case.py | 28 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/data/cases/line_ranges_diff_edge_case.py diff --git a/CHANGES.md b/CHANGES.md index 4c3fbf1afc8..18bab5131e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,9 @@ +- `--line-ranges` now skips _Black_'s internal stability check in `--safe` mode. This + avoids a crash on rare inputs that have many unformatted same-content lines. (#4034) + ### Packaging - Upgrade to mypy 1.6.1 (#4049) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 546fdc474e8..0c1a4d3b5a1 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -196,6 +196,12 @@ Example: `black --line-ranges=1-10 --line-ranges=21-30 test.py` will format line This option is mainly for editor integrations, such as "Format Selection". +```{note} +Due to [#4052](https://github.com/psf/black/issues/4052), `--line-ranges` might format +extra lines outside of the ranges when ther are unformatted lines with the exact +content. It also disables _Black_'s formatting stability check in `--safe` mode. +``` + #### `--color` / `--no-color` Show (or do not show) colored diff. Only applies when `--diff` is given. diff --git a/src/black/__init__.py b/src/black/__init__.py index 2455e8648fc..b33beeeeb23 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1465,11 +1465,16 @@ def assert_stable( src: str, dst: str, mode: Mode, *, lines: Collection[Tuple[int, int]] = () ) -> None: """Raise AssertionError if `dst` reformats differently the second time.""" + if lines: + # Formatting specified lines requires `adjusted_lines` to map original lines + # to the formatted lines before re-formatting the previously formatted result. + # Due to less-ideal diff algorithm, some edge cases produce incorrect new line + # ranges. Hence for now, we skip the stable check. + # See https://github.com/psf/black/issues/4033 for context. + return # 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. - if lines: - lines = adjusted_lines(lines, src, dst) newdst = _format_str_once(dst, mode=mode, lines=lines) if dst != newdst: log = dump_to_file( diff --git a/tests/data/cases/line_ranges_diff_edge_case.py b/tests/data/cases/line_ranges_diff_edge_case.py new file mode 100644 index 00000000000..f5cb2d0bb5f --- /dev/null +++ b/tests/data/cases/line_ranges_diff_edge_case.py @@ -0,0 +1,28 @@ +# flags: --line-ranges=10-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Reproducible example for https://github.com/psf/black/issues/4033. +# This can be fixed in the future if we use a better diffing algorithm, or make Black +# perform formatting in a single pass. + +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) + +# output +# flags: --line-ranges=10-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Reproducible example for https://github.com/psf/black/issues/4033. +# This can be fixed in the future if we use a better diffing algorithm, or make Black +# perform formatting in a single pass. + +print ( "format me" ) +print("format me") +print("format me") +print("format me") +print("format me") From be336bb67fb6c12667836f7fba4993f9be9c61dd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 22:33:16 -0800 Subject: [PATCH 646/700] Run lint job on Ubuntu only (#4061) --- .github/workflows/lint.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d1ad23bb2ab..9c7aca8f869 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,11 +11,7 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From fb5e5d2be6367a0402a60da94f139dfb8943ed37 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Thu, 23 Nov 2023 05:11:49 +0200 Subject: [PATCH 647/700] Prefer more equal signs before a break when splitting chained assignments (#4010) Fixes #4007 --- CHANGES.md | 2 +- src/black/linegen.py | 64 ++++++++++++++------ src/black/lines.py | 5 ++ tests/data/cases/preview_prefer_rhs_split.py | 21 +++++++ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 18bab5131e6..6a8b97c75eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,8 +12,8 @@ ### Preview style +- Prefer more equal signs before a break when splitting chained assignments (#4010) - Standalone form feed characters at the module level are no longer removed (#4021) - - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) diff --git a/src/black/linegen.py b/src/black/linegen.py index 7fbbe290d7e..7152568783e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -910,24 +910,32 @@ def _maybe_split_omitting_optional_parens( try: # The RHSResult Omitting Optional Parens. rhs_oop = _first_right_hand_split(line, omit=omit) - if not ( + prefer_splitting_rhs_mode = ( Preview.prefer_splitting_right_hand_side_of_assignments in line.mode - # the split is right after `=` - and len(rhs.head.leaves) >= 2 - and rhs.head.leaves[-2].type == token.EQUAL - # the left side of assignment contains brackets - and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) - # the left side of assignment is short enough (the -1 is for the ending - # optional paren) - and is_line_short_enough( - rhs.head, mode=replace(mode, line_length=mode.line_length - 1) + ) + is_split_right_after_equal = ( + len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL + ) + rhs_head_contains_brackets = any( + leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1] + ) + # the -1 is for the ending optional paren + rhs_head_short_enough = is_line_short_enough( + rhs.head, mode=replace(mode, line_length=mode.line_length - 1) + ) + rhs_head_explode_blocked_by_magic_trailing_comma = ( + rhs.head.magic_trailing_comma is None + ) + if ( + not ( + prefer_splitting_rhs_mode + and is_split_right_after_equal + and rhs_head_contains_brackets + and rhs_head_short_enough + and rhs_head_explode_blocked_by_magic_trailing_comma ) - # the left side of assignment won't explode further because of magic - # trailing comma - and rhs.head.magic_trailing_comma is None - # the split by omitting optional parens isn't preferred by some other - # reason - and not _prefer_split_rhs_oop(rhs_oop, mode) + # the omit optional parens split is preferred by some other reason + or _prefer_split_rhs_oop_over_rhs(rhs_oop, rhs, mode) ): yield from _maybe_split_omitting_optional_parens( rhs_oop, line, mode, features=features, omit=omit @@ -935,8 +943,12 @@ def _maybe_split_omitting_optional_parens( return except CannotSplit as e: - if not ( - can_be_split(rhs.body) or is_line_short_enough(rhs.body, mode=mode) + # For chained assignments we want to use the previous successful split + if line.is_chained_assignment: + pass + + elif not can_be_split(rhs.body) and not is_line_short_enough( + rhs.body, mode=mode ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." @@ -960,10 +972,22 @@ def _maybe_split_omitting_optional_parens( yield result -def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool: +def _prefer_split_rhs_oop_over_rhs( + rhs_oop: RHSResult, rhs: RHSResult, mode: Mode +) -> bool: """ - Returns whether we should prefer the result from a split omitting optional parens. + Returns whether we should prefer the result from a split omitting optional parens + (rhs_oop) over the original (rhs). """ + # If we have multiple targets, we prefer more `=`s on the head vs pushing them to + # the body + rhs_head_equal_count = [leaf.type for leaf in rhs.head.leaves].count(token.EQUAL) + rhs_oop_head_equal_count = [leaf.type for leaf in rhs_oop.head.leaves].count( + token.EQUAL + ) + if rhs_head_equal_count > 1 and rhs_head_equal_count > rhs_oop_head_equal_count: + return False + has_closing_bracket_after_assign = False for leaf in reversed(rhs_oop.head.leaves): if leaf.type == token.EQUAL: diff --git a/src/black/lines.py b/src/black/lines.py index ec6145ff848..6e33ee57eab 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -209,6 +209,11 @@ def is_triple_quoted_string(self) -> bool: return True return False + @property + def is_chained_assignment(self) -> bool: + """Is the line a chained assignment""" + return [leaf.type for leaf in self.leaves].count(token.EQUAL) > 1 + @property def opens_block(self) -> bool: """Does this line open a new level of indentation.""" diff --git a/tests/data/cases/preview_prefer_rhs_split.py b/tests/data/cases/preview_prefer_rhs_split.py index c732c33b53a..28d89c368c0 100644 --- a/tests/data/cases/preview_prefer_rhs_split.py +++ b/tests/data/cases/preview_prefer_rhs_split.py @@ -84,3 +84,24 @@ ) or ( isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing ) + +# Multiple targets +a = b = ( + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) + +a = b = c = d = e = f = g = ( + hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh +) = i = j = ( + kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) + +a = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) = c + +a = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) = ( + cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) = ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd From 69d49c5a6fe064eb290dd6445745fdeb2643f54f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Nov 2023 14:19:54 +0000 Subject: [PATCH 648/700] Bump mypy to 1.7.1 (#4069) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 2 +- pyproject.toml | 4 ++-- src/blib2to3/pgen2/tokenize.py | 7 ++----- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c153746b621..2896489d724 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.1 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index 6a8b97c75eb..f6fe69bac68 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,7 +26,7 @@ ### Packaging -- Upgrade to mypy 1.6.1 (#4049) +- Upgrade to mypy 1.7.1 (#4049) (#4069) ### Parser diff --git a/pyproject.toml b/pyproject.toml index e63e0aea3ef..6b681e8226a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ macos-max-compat = true enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.6.1", + "mypy==1.7.1", "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true @@ -187,7 +187,7 @@ CC = "clang" build-frontend = { name = "build", args = ["--no-isolation"] } # Unfortunately, hatch doesn't respect MACOSX_DEPLOYMENT_TARGET before-build = [ - "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.6.1' 'click==8.1.3'", + "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.7.1' 'click==8.1.3'", """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, ] diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index d0607f4b1e1..b04b18ba870 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -39,7 +39,6 @@ Set, Tuple, Union, - cast, ) from blib2to3.pgen2.grammar import Grammar @@ -262,11 +261,9 @@ def add_whitespace(self, start: Coord) -> None: def untokenize(self, iterable: Iterable[TokenInfo]) -> str: for t in iterable: if len(t) == 2: - self.compat(cast(Tuple[int, str], t), iterable) + self.compat(t, iterable) break - tok_type, token, start, end, line = cast( - Tuple[int, str, Coord, Coord, str], t - ) + tok_type, token, start, end, line = t self.add_whitespace(start) self.tokens.append(token) self.prev_row, self.prev_col = end From a0e270d0f246387202e676b25abbf7a02ddcbc71 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Nov 2023 18:05:59 +0000 Subject: [PATCH 649/700] Build mypycified wheels for Python 3.12 (#4070) --- .github/workflows/pypi_upload.yml | 2 +- CHANGES.md | 1 + pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 07273f09508..bbdcdf17a8f 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -73,7 +73,7 @@ jobs: | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix env: - CIBW_BUILD: "cp38-* cp311-*" + CIBW_BUILD: "cp38-* cp312-*" CIBW_ARCHS_LINUX: x86_64 - id: set-matrix run: echo "include=$(cat /tmp/matrix)" | tee -a $GITHUB_OUTPUT diff --git a/CHANGES.md b/CHANGES.md index f6fe69bac68..e9ffd6bb9f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ ### Packaging - Upgrade to mypy 1.7.1 (#4049) (#4069) +- Faster compiled wheels are now available for CPython 3.12 (#4070) ### Parser diff --git a/pyproject.toml b/pyproject.toml index 6b681e8226a..1098412981a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ build-verbosity = 1 # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS build = "cp3*" -skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*", "cp312-*"] +skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*"] # This is the bare minimum needed to run the test suite. Pulling in the full # test_requirements.txt would download a bunch of other packages not necessary # here and would slow down the testing step a fair bit. From 66ec056e39da957d3c82da5b7a86ef228606cfe6 Mon Sep 17 00:00:00 2001 From: exag Date: Mon, 4 Dec 2023 14:47:30 +0900 Subject: [PATCH 650/700] Fix minor typos in docstrings (#4085) --- src/black/numerics.py | 2 +- src/black/ranges.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/black/numerics.py b/src/black/numerics.py index 67ac8595fcc..3040de06fde 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -14,7 +14,7 @@ def format_hex(text: str) -> str: def format_scientific_notation(text: str) -> str: - """Formats a numeric string utilizing scentific notation""" + """Formats a numeric string utilizing scientific notation""" before, after = text.split("e") sign = "" if after.startswith("-"): diff --git a/src/black/ranges.py b/src/black/ranges.py index b0c312e6274..59e19242d47 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -172,7 +172,7 @@ class _TopLevelStatementsVisitor(Visitor[None]): A node visitor that converts unchanged top-level statements to STANDALONE_COMMENT. - This is used in addition to _convert_unchanged_lines_by_flatterning, to + This is used in addition to _convert_unchanged_line_by_line, to speed up formatting when there are unchanged top-level classes/functions/statements. """ @@ -302,7 +302,7 @@ def _convert_node_to_standalone_comment(node: LN) -> None: index = node.remove() if index is not None: # Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when - # genearting the formatted code. + # generating the formatted code. value = str(node)[:-1] parent.insert_child( index, From 3416b2c82d51f27ce55c31ef0bfe4a9e21816611 Mon Sep 17 00:00:00 2001 From: Riyazuddin Khan Date: Mon, 4 Dec 2023 23:40:03 +0530 Subject: [PATCH 651/700] Fix: --line-ranges dedents a # fmt: off in the middle of a decorator (#4084) Fixes #4068 --- CHANGES.md | 3 ++- src/black/__init__.py | 2 +- src/black/comments.py | 23 ++++++++++++++---- .../cases/line_ranges_fmt_off_decorator.py | 24 +++++++++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e9ffd6bb9f5..f17cd7fdc9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,8 @@ ### Stable style - +- Fix bug where `# fmt: off` automatically dedents when used with the `--line-ranges` + option, even when it is not within the specified line range. (#4084) ### Preview style diff --git a/src/black/__init__.py b/src/black/__init__.py index b33beeeeb23..04f6d8c58de 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1180,7 +1180,7 @@ def _format_str_once( for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} if supports_feature(versions, feature) } - normalize_fmt_off(src_node, mode) + normalize_fmt_off(src_node, mode, lines) if lines: # This should be called after normalize_fmt_off. convert_unchanged_lines(src_node, lines) diff --git a/src/black/comments.py b/src/black/comments.py index 8a0e925fdc0..25413121199 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,7 +1,7 @@ import re from dataclasses import dataclass from functools import lru_cache -from typing import Final, Iterator, List, Optional, Union +from typing import Collection, Final, Iterator, List, Optional, Tuple, Union from black.mode import Mode, Preview from black.nodes import ( @@ -161,14 +161,18 @@ def make_comment(content: str) -> str: return "#" + content -def normalize_fmt_off(node: Node, mode: Mode) -> None: +def normalize_fmt_off( + node: Node, mode: Mode, lines: Collection[Tuple[int, int]] +) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node, mode) + try_again = convert_one_fmt_off_pair(node, mode, lines) -def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: +def convert_one_fmt_off_pair( + node: Node, mode: Mode, lines: Collection[Tuple[int, int]] +) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. @@ -213,7 +217,18 @@ def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: prefix[:previous_consumed] + "\n" * comment.newlines ) hidden_value = "".join(str(n) for n in ignored_nodes) + comment_lineno = leaf.lineno - comment.newlines if comment.value in FMT_OFF: + fmt_off_prefix = "" + if len(lines) > 0 and not any( + comment_lineno >= line[0] and comment_lineno <= line[1] + for line in lines + ): + # keeping indentation of comment by preserving original whitespaces. + fmt_off_prefix = prefix.split(comment.value)[0] + if "\n" in fmt_off_prefix: + fmt_off_prefix = fmt_off_prefix.split("\n")[-1] + standalone_comment_prefix += fmt_off_prefix hidden_value = comment.value + "\n" + hidden_value if _contains_fmt_skip_comment(comment.value, mode): hidden_value += " " + comment.value diff --git a/tests/data/cases/line_ranges_fmt_off_decorator.py b/tests/data/cases/line_ranges_fmt_off_decorator.py index 14aa1dda02d..065bf4328d7 100644 --- a/tests/data/cases/line_ranges_fmt_off_decorator.py +++ b/tests/data/cases/line_ranges_fmt_off_decorator.py @@ -1,4 +1,4 @@ -# flags: --line-ranges=12-12 +# flags: --line-ranges=12-12 --line-ranges=21-21 # NOTE: If you need to modify this file, pay special attention to the --line-ranges= # flag above as it's formatting specifically these lines. @@ -11,9 +11,19 @@ class MyClass: def method(): print ( "str" ) + @decor( + a=1, + # fmt: off + b=(2, 3), + # fmt: on + ) + def func(): + pass + + # output -# flags: --line-ranges=12-12 +# flags: --line-ranges=12-12 --line-ranges=21-21 # NOTE: If you need to modify this file, pay special attention to the --line-ranges= # flag above as it's formatting specifically these lines. @@ -25,3 +35,13 @@ class MyClass: # fmt: on def method(): print("str") + + @decor( + a=1, + # fmt: off + b=(2, 3), + # fmt: on + ) + def func(): + pass + From 50d5756e8e63b17e4523f096f312011273ce640f Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:19:24 +0100 Subject: [PATCH 652/700] fix crash in preview mode with --line-length=1 (#4086) --- CHANGES.md | 1 + src/black/linegen.py | 2 +- .../return_annotation_brackets_crash_line_length_1.py | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/return_annotation_brackets_crash_line_length_1.py diff --git a/CHANGES.md b/CHANGES.md index f17cd7fdc9d..8f0b75e7f10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) +- Fix crash in preview mode when using a short `--line-length` (#4086) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 7152568783e..073672a5ae7 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -744,7 +744,7 @@ def left_hand_split( if leaf.type in OPENING_BRACKETS: matching_bracket = leaf current_leaves = body_leaves - if not matching_bracket: + if not matching_bracket or not tail_leaves: raise CannotSplit("No brackets found") head = bracket_split_build_line( diff --git a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py new file mode 100644 index 00000000000..9d96b4ab97a --- /dev/null +++ b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py @@ -0,0 +1,9 @@ +# flags: --preview --minimum-version=3.10 --line-length=1 + +def foo() -> tuple[int, int,]: + ... +# output +def foo() -> tuple[ + int, + int, +]: ... From e4ae213f06050e7f76ebcf01578c002e412dafdc Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:17:33 +0100 Subject: [PATCH 653/700] test preview cases with line-length 1 unless explicitly skipped (#4087) * Add new flag for tests, --no-preview-line-length-1, to be used for test cases known to not work in preview mode with line-length=1. Also split out the problematic cases in three cases to separate files. Removed now redundant file which explicitly tested preview annotations with line-length=1 * mode.preview -> preview_mode, mark pep_572_remove_parens as failing with ll1 --- tests/data/cases/comment_type_hint.py | 3 + tests/data/cases/comments2.py | 4 - tests/data/cases/fmtskip2.py | 5 +- tests/data/cases/pep_572_remove_parens.py | 2 +- ..._parens_with_braces_and_square_brackets.py | 96 ---------------- ..._with_braces_and_square_brackets_no_ll1.py | 106 ++++++++++++++++++ ...annotation_brackets_crash_line_length_1.py | 9 -- tests/test_format.py | 2 + tests/util.py | 54 ++++++--- 9 files changed, 154 insertions(+), 127 deletions(-) create mode 100644 tests/data/cases/comment_type_hint.py create mode 100644 tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py delete mode 100644 tests/data/cases/return_annotation_brackets_crash_line_length_1.py diff --git a/tests/data/cases/comment_type_hint.py b/tests/data/cases/comment_type_hint.py new file mode 100644 index 00000000000..2992da88d90 --- /dev/null +++ b/tests/data/cases/comment_type_hint.py @@ -0,0 +1,3 @@ +# flags: --no-preview-line-length-1 +# split out from comments2 as it does not work with line-length=1, losing the comment +a = "type comment with trailing space" # type: str diff --git a/tests/data/cases/comments2.py b/tests/data/cases/comments2.py index 1487dc4b6e2..261c5e9f0a0 100644 --- a/tests/data/cases/comments2.py +++ b/tests/data/cases/comments2.py @@ -155,8 +155,6 @@ def _init_host(self, parsed) -> None: pass -a = "type comment with trailing space" # type: str - ####################### ### SECTION COMMENT ### ####################### @@ -335,8 +333,6 @@ def _init_host(self, parsed) -> None: pass -a = "type comment with trailing space" # type: str - ####################### ### SECTION COMMENT ### ####################### diff --git a/tests/data/cases/fmtskip2.py b/tests/data/cases/fmtskip2.py index e6248117aa9..0189d4e642d 100644 --- a/tests/data/cases/fmtskip2.py +++ b/tests/data/cases/fmtskip2.py @@ -1,9 +1,12 @@ +# flags: --no-preview-line-length-1 +# l2 loses the comment with line-length=1 in preview mode l1 = ["This list should be broken up", "into multiple lines", "because it is way too long"] l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip l3 = ["I have", "trailing comma", "so I should be braked",] # output +# l2 loses the comment with line-length=1 in preview mode l1 = [ "This list should be broken up", "into multiple lines", @@ -14,4 +17,4 @@ "I have", "trailing comma", "so I should be braked", -] \ No newline at end of file +] diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 24f1ac29168..88774d81649 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -1,4 +1,4 @@ -# flags: --minimum-version=3.8 +# flags: --minimum-version=3.8 --no-preview-line-length-1 if (foo := 0): pass diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 9e5c9eb8546..47a6a0bcae6 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -125,23 +125,6 @@ def foo_square_brackets(request): func([x for x in "long line long line long line long line long line long line long line"]) func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) -func({"short line"}) -func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) -func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) -func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) -func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) -func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) - -# Do not hug if the argument fits on a single line. -func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) -func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) -func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) -func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) -func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) -array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] -array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] -array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] - foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} ) @@ -151,14 +134,11 @@ def foo_square_brackets(request): ) nested_mapping = {"key": [{"a very long key 1": "with a very long value", "a very long key 2": "with a very long value"}]} -nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] explicit_exploding = [[["short", "line",],],] single_item_do_not_explode = Context({ "version": get_docs_version(), }) -foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) - foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) foo( @@ -310,69 +290,6 @@ def foo_square_brackets(request): ] ]) -func({"short line"}) -func({ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -}) -func({{ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -}}) -func(( - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -)) -func((( - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -))) -func([[ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -]]) - -# Do not hug if the argument fits on a single line. -func( - {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} -) -func( - ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") -) -func( - ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] -) -func( - **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} -) -func( - *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") -) -array = [ - {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} -] -array = [ - ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") -] -array = [ - ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] -] - foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} ) @@ -387,13 +304,6 @@ def foo_square_brackets(request): "a very long key 2": "with a very long value", }] } -nested_array = [[[ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -]]] explicit_exploding = [ [ [ @@ -406,12 +316,6 @@ def foo_square_brackets(request): "version": get_docs_version(), }) -foo(*[ - "long long long long long line", - "long long long long long line", - "long long long long long line", -]) - foo(*[ str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) ]) diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py new file mode 100644 index 00000000000..fdebdf69c20 --- /dev/null +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py @@ -0,0 +1,106 @@ +# flags: --preview --no-preview-line-length-1 +# split out from preview_hug_parens_with_brackes_and_square_brackets, as it produces +# different code on the second pass with line-length 1 in many cases. +# Seems to be about whether the last string in a sequence gets wrapped in parens or not. +foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) +func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) +func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) +func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) + + +# Do not hug if the argument fits on a single line. +func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) +func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) +func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) +func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) +func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) +array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] +array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] +array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] + +nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] + +# output + +# split out from preview_hug_parens_with_brackes_and_square_brackets, as it produces +# different code on the second pass with line-length 1 in many cases. +# Seems to be about whether the last string in a sequence gets wrapped in parens or not. +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({{ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}}) +func(( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +)) +func((( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +))) +func([[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]) + + +# Do not hug if the argument fits on a single line. +func( + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +) +func( + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +) +func( + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +) +func( + **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} +) +func( + *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") +) +array = [ + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +] +array = [ + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +] +array = [ + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +] + +nested_array = [[[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]] diff --git a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py deleted file mode 100644 index 9d96b4ab97a..00000000000 --- a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py +++ /dev/null @@ -1,9 +0,0 @@ -# flags: --preview --minimum-version=3.10 --line-length=1 - -def foo() -> tuple[int, int,]: - ... -# output -def foo() -> tuple[ - int, - int, -]: ... diff --git a/tests/test_format.py b/tests/test_format.py index 6c2eca8c618..9162c585c08 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -30,6 +30,7 @@ def check_file(subdir: str, filename: str, *, data: bool = True) -> None: fast=args.fast, minimum_version=args.minimum_version, lines=args.lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) if args.minimum_version is not None: major, minor = args.minimum_version @@ -42,6 +43,7 @@ def check_file(subdir: str, filename: str, *, data: bool = True) -> None: fast=args.fast, minimum_version=args.minimum_version, lines=args.lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) diff --git a/tests/util.py b/tests/util.py index c8699d335ab..9ea30e62fe3 100644 --- a/tests/util.py +++ b/tests/util.py @@ -46,6 +46,7 @@ class TestCaseArgs: fast: bool = False minimum_version: Optional[Tuple[int, int]] = None lines: Collection[Tuple[int, int]] = () + no_preview_line_length_1: bool = False def _assert_format_equal(expected: str, actual: str) -> None: @@ -96,6 +97,7 @@ def assert_format( fast: bool = False, minimum_version: Optional[Tuple[int, int]] = None, lines: Collection[Tuple[int, int]] = (), + no_preview_line_length_1: bool = False, ) -> None: """Convenience function to check that Black formats as expected. @@ -124,21 +126,28 @@ def assert_format( f"Black crashed formatting this case in {text} mode." ) from e # Similarly, setting line length to 1 is a good way to catch - # stability bugs. But only in non-preview mode because preview mode - # currently has a lot of line length 1 bugs. - try: - _assert_format_inner( - source, - None, - replace(mode, preview=False, line_length=1), - fast=fast, - minimum_version=minimum_version, - lines=lines, - ) - except Exception as e: - raise FormatFailure( - "Black crashed formatting this case with line-length set to 1." - ) from e + # stability bugs. Some tests are known to be broken in preview mode with line length + # of 1 though, and have marked that with a flag --no-preview-line-length-1 + preview_modes = [False] + if not no_preview_line_length_1: + preview_modes.append(True) + + for preview_mode in preview_modes: + + try: + _assert_format_inner( + source, + None, + replace(mode, preview=preview_mode, line_length=1), + fast=fast, + minimum_version=minimum_version, + lines=lines, + ) + except Exception as e: + text = "preview" if preview_mode else "non-preview" + raise FormatFailure( + f"Black crashed formatting this case in {text} mode with line-length=1." + ) from e def _assert_format_inner( @@ -246,6 +255,15 @@ def get_flags_parser() -> argparse.ArgumentParser: ), ) parser.add_argument("--line-ranges", action="append") + parser.add_argument( + "--no-preview-line-length-1", + default=False, + action="store_true", + help=( + "Don't run in preview mode with --line-length=1, as that's known to cause a" + " crash" + ), + ) return parser @@ -266,7 +284,11 @@ def parse_mode(flags_line: str) -> TestCaseArgs: else: lines = [] return TestCaseArgs( - mode=mode, fast=args.fast, minimum_version=args.minimum_version, lines=lines + mode=mode, + fast=args.fast, + minimum_version=args.minimum_version, + lines=lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) From 50e287cecea41ee32bd66ab1eee4827f6b8312ce Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:38:57 -0600 Subject: [PATCH 654/700] docs: Clarify include/exclude documentation (#4072) --- docs/usage_and_configuration/the_basics.md | 22 +++++++-------- src/black/__init__.py | 33 +++++++++++----------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 0c1a4d3b5a1..3739bcaefa1 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,24 +241,17 @@ Because of our [stability policy](../the_black_code_style/index.md), this will g stable formatting, but still allow you to take advantage of improvements that do not affect formatting. -#### `--include` - -A regular expression that matches files and directories that should be included on -recursive searches. An empty value means all files are included regardless of the name. -Use forward slashes for directories on all platforms (Windows, too). Exclusions are -calculated first, inclusions later. - #### `--exclude` A regular expression that matches files and directories that should be excluded on recursive searches. An empty value means no paths are excluded. Use forward slashes for -directories on all platforms (Windows, too). Exclusions are calculated first, inclusions -later. +directories on all platforms (Windows, too). By default, Black also ignores all paths +listed in `.gitignore`. Changing this value will override all default exclusions. #### `--extend-exclude` -Like `--exclude`, but adds additional files and directories on top of the excluded ones. -Useful if you simply want to add to the default. +Like `--exclude`, but adds additional files and directories on top of the default values +instead of overriding them. #### `--force-exclude` @@ -271,6 +264,13 @@ programmatically on changed files, such as in a pre-commit hook or editor plugin The name of the file when passing it through stdin. Useful to make sure Black will respect the `--force-exclude` option on some editors that rely on using stdin. +#### `--include` + +A regular expression that matches files and directories that should be included on +recursive searches. An empty value means all files are included regardless of the name. +Use forward slashes for directories on all platforms (Windows, too). Overrides all +exclusions, including from `.gitignore` and command line options. + #### `-W`, `--workers` When _Black_ formats multiple files, it may use a process pool to speed up formatting. diff --git a/src/black/__init__.py b/src/black/__init__.py index 04f6d8c58de..e7dac895a6a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -344,19 +344,6 @@ def validate_regex( " either a major version number or an exact version." ), ) -@click.option( - "--include", - type=str, - default=DEFAULT_INCLUDES, - callback=validate_regex, - help=( - "A regular expression that matches files and directories that should be" - " included on recursive searches. An empty value means all files are included" - " regardless of the name. Use forward slashes for directories on all platforms" - " (Windows, too). Exclusions are calculated first, inclusions later." - ), - show_default=True, -) @click.option( "--exclude", type=str, @@ -365,8 +352,8 @@ def validate_regex( "A regular expression that matches files and directories that should be" " excluded on recursive searches. An empty value means no paths are excluded." " Use forward slashes for directories on all platforms (Windows, too)." - " Exclusions are calculated first, inclusions later. [default:" - f" {DEFAULT_EXCLUDES}]" + " By default, Black also ignores all paths listed in .gitignore. Changing this" + f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]" ), show_default=False, ) @@ -376,7 +363,7 @@ def validate_regex( callback=validate_regex, help=( "Like --exclude, but adds additional files and directories on top of the" - " excluded ones. (Useful if you simply want to add to the default)" + " default values instead of overriding them." ), ) @click.option( @@ -398,6 +385,20 @@ def validate_regex( "editors that rely on using stdin." ), ) +@click.option( + "--include", + type=str, + default=DEFAULT_INCLUDES, + callback=validate_regex, + help=( + "A regular expression that matches files and directories that should be" + " included on recursive searches. An empty value means all files are included" + " regardless of the name. Use forward slashes for directories on all platforms" + " (Windows, too). Overrides all exclusions, including from .gitignore and" + " command line options." + ), + show_default=True, +) @click.option( "-W", "--workers", From 432d9050c3d1e35a36ffc97d4a9e0e0c9e5e4ecc Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:32:06 -0600 Subject: [PATCH 655/700] docs: Unify option descriptions between `--help` and `the_basics.md` (#4076) --- docs/usage_and_configuration/the_basics.md | 45 +++++++------ src/black/__init__.py | 75 +++++++++++++--------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 3739bcaefa1..73c0d1323e3 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -35,6 +35,10 @@ are deliberately limited and rarely added. Note that all command-line options listed above can also be configured using a `pyproject.toml` file (more on that below). +#### `-h`, `--help` + +Show available command-line options and exit. + #### `-c`, `--code` Format the code passed in as a string. @@ -109,6 +113,10 @@ useful when piping source on standard input. When processing Jupyter Notebooks, add the given magic to the list of known python- magics. Useful for formatting cells with custom python magics. +#### `-x, --skip-source-first-line` + +Skip the first line of the source code. + #### `-S, --skip-string-normalization` By default, _Black_ uses double quotes for all strings and normalizes string prefixes, @@ -132,7 +140,7 @@ functionality in the next major release. Read more about #### `--check` -Passing `--check` will make _Black_ exit with: +Don't write the files back, just return the status. _Black_ will exit with: - code 0 if nothing would change; - code 1 if some files would be reformatted; or @@ -162,8 +170,8 @@ $ echo $? #### `--diff` -Passing `--diff` will make _Black_ print out diffs that indicate what changes _Black_ -would've made. They are printed to stdout so capturing them is simple. +Don't write the files back, just output a diff to indicate what changes _Black_ would've +made. They are printed to stdout so capturing them is simple. If you'd like colored diffs, you can enable them with `--color`. @@ -179,6 +187,10 @@ All done! ✨ 🍰 ✨ 1 file would be reformatted. ``` +#### `--color` / `--no-color` + +Show (or do not show) colored diff. Only applies when `--diff` is given. + ### `--line-ranges` When specified, _Black_ will try its best to only format these lines. @@ -202,10 +214,6 @@ extra lines outside of the ranges when ther are unformatted lines with the exact content. It also disables _Black_'s formatting stability check in `--safe` mode. ``` -#### `--color` / `--no-color` - -Show (or do not show) colored diff. Only applies when `--diff` is given. - #### `--fast` / `--safe` By default, _Black_ performs [an AST safety check](labels/ast-changes) after formatting @@ -256,7 +264,7 @@ instead of overriding them. #### `--force-exclude` Like `--exclude`, but files and directories matching this regex will be excluded even -when they are passed explicitly as arguments. This is useful when invoking _Black_ +when they are passed explicitly as arguments. This is useful when invoking Black programmatically on changed files, such as in a pre-commit hook or editor plugin. #### `--stdin-filename` @@ -275,12 +283,12 @@ exclusions, including from `.gitignore` and command line options. When _Black_ formats multiple files, it may use a process pool to speed up formatting. This option controls the number of parallel workers. This can also be specified via the -`BLACK_NUM_WORKERS` environment variable. +`BLACK_NUM_WORKERS` environment variable. Defaults to the number of CPUs in the system. #### `-q`, `--quiet` -Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critical output. -Error messages will still be emitted (which can silenced by `2>/dev/null`). +Stop emitting all non-critical output. Error messages will still be emitted (which can +silenced by `2>/dev/null`). ```console $ black src/ -q @@ -289,9 +297,9 @@ error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio #### `-v`, `--verbose` -Passing `-v` / `--verbose` will cause _Black_ to also emit messages about files that -were not changed or were ignored due to exclusion patterns. If _Black_ is using a -configuration file, a blue message detailing which one it is using will be emitted. +Emit messages about files that were not changed or were ignored due to exclusion +patterns. If _Black_ is using a configuration file, a message detailing which one it is +using will be emitted. ```console $ black src/ -v @@ -321,10 +329,6 @@ black, 23.11.0 Read configuration options from a configuration file. See [below](#configuration-via-a-file) for more details on the configuration file. -#### `-h`, `--help` - -Show available command-line options and exit. - ### Environment variable options _Black_ supports the following configuration via environment variables. @@ -355,7 +359,7 @@ All done! ✨ 🍰 ✨ use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude` option on some editors that rely on using stdin. -You can also pass code as a string using the `-c` / `--code` option. +You can also pass code as a string using the `--code` option. ### Writeback and reporting @@ -435,8 +439,7 @@ refers to the path to your home directory. On Windows, this will be something li You can also explicitly specify the path to a particular file that you want with `--config`. In this situation _Black_ will not look for any other file. -If you're running with `--verbose`, you will see a blue message if a file was found and -used. +If you're running with `--verbose`, you will see a message if a file was found and used. Please note `blackd` will not use `pyproject.toml` configuration. diff --git a/src/black/__init__.py b/src/black/__init__.py index e7dac895a6a..5073fa748d5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -235,25 +235,26 @@ def validate_regex( callback=target_version_option_callback, multiple=True, help=( - "Python versions that should be supported by Black's output. By default, Black" - " will try to infer this from the project metadata in pyproject.toml. If this" - " does not yield conclusive results, Black will use per-file auto-detection." + "Python versions that should be supported by Black's output. You should" + " include all versions that your code supports. By default, Black will infer" + " target versions from the project metadata in pyproject.toml. If this does" + " not yield conclusive results, Black will use per-file auto-detection." ), ) @click.option( "--pyi", is_flag=True, help=( - "Format all input files like typing stubs regardless of file extension (useful" - " when piping source on standard input)." + "Format all input files like typing stubs regardless of file extension. This" + " is useful when piping source on standard input." ), ) @click.option( "--ipynb", is_flag=True, help=( - "Format all input files like Jupyter Notebooks regardless of file extension " - "(useful when piping source on standard input)." + "Format all input files like Jupyter Notebooks regardless of file extension." + "This is useful when piping source on standard input." ), ) @click.option( @@ -310,14 +311,22 @@ def validate_regex( @click.option( "--diff", is_flag=True, - help="Don't write the files back, just output a diff for each file on stdout.", + help=( + "Don't write the files back, just output a diff to indicate what changes" + " Black would've made. They are printed to stdout so capturing them is simple." + ), +) +@click.option( + "--color/--no-color", + is_flag=True, + help="Show (or do not show) colored diff. Only applies when --diff is given.", ) @click.option( "--line-ranges", multiple=True, metavar="START-END", help=( - "When specified, _Black_ will try its best to only format these lines. This" + "When specified, Black will try its best to only format these lines. This" " option can be specified multiple times, and a union of the lines will be" " formatted. Each range must be specified as two integers connected by a `-`:" " `-`. The `` and `` integer indices are 1-based and" @@ -325,23 +334,24 @@ def validate_regex( ), default=(), ) -@click.option( - "--color/--no-color", - is_flag=True, - help="Show colored diff. Only applies when `--diff` is given.", -) @click.option( "--fast/--safe", is_flag=True, - help="If --fast given, skip temporary sanity checks. [default: --safe]", + help=( + "By default, Black performs an AST safety check after formatting your code." + " The --fast flag turns off this check and the --safe flag explicitly enables" + " it. [default: --safe]" + ), ) @click.option( "--required-version", 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). It can be" - " either a major version number or an exact version." + "Require a specific version of Black to be running. This is useful for" + " ensuring that all contributors to your project are using the same" + " version, because different versions of Black may format code a little" + " differently. This option can be set in a configuration file for consistent" + " results across environments." ), ) @click.option( @@ -371,8 +381,10 @@ def validate_regex( type=str, callback=validate_regex, help=( - "Like --exclude, but files and directories matching this regex will be " - "excluded even when they are passed explicitly as arguments." + "Like --exclude, but files and directories matching this regex will be excluded" + " even when they are passed explicitly as arguments. This is useful when" + " invoking Black programmatically on changed files, such as in a pre-commit" + " hook or editor plugin." ), ) @click.option( @@ -380,9 +392,9 @@ def validate_regex( type=str, is_eager=True, help=( - "The name of the file when passing it through stdin. Useful to make " - "sure Black will respect --force-exclude option on some " - "editors that rely on using stdin." + "The name of the file when passing it through stdin. Useful to make sure Black" + " will respect the --force-exclude option on some editors that rely on using" + " stdin." ), ) @click.option( @@ -405,8 +417,10 @@ def validate_regex( type=click.IntRange(min=1), default=None, help=( - "Number of parallel workers [default: BLACK_NUM_WORKERS environment variable " - "or number of CPUs in the system]" + "When Black formats multiple files, it may use a process pool to speed up" + " formatting. This option controls the number of parallel workers. This can" + " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults" + " to the number of CPUs in the system." ), ) @click.option( @@ -414,8 +428,8 @@ def validate_regex( "--quiet", is_flag=True, help=( - "Don't emit non-error messages to stderr. Errors are still emitted; silence" - " those with 2>/dev/null." + "Stop emitting all non-critical output. Error messages will still be emitted" + " (which can silenced by 2>/dev/null)." ), ) @click.option( @@ -423,8 +437,9 @@ def validate_regex( "--verbose", is_flag=True, help=( - "Also emit messages to stderr about files that were not changed or were ignored" - " due to exclusion patterns." + "Emit messages about files that were not changed or were ignored due to" + " exclusion patterns. If Black is using a configuration file, a message" + " detailing which one it is using will be emitted." ), ) @click.version_option( @@ -455,7 +470,7 @@ def validate_regex( ), is_eager=True, callback=read_pyproject_toml, - help="Read configuration from FILE path.", + help="Read configuration options from a configuration file.", ) @click.pass_context def main( # noqa: C901 From e7e122e9ff27fc040a6e8ecd92f0e7603c87f92d Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:44:15 -0600 Subject: [PATCH 656/700] docs: Move `fmt: off` docs (#4090) --- docs/the_black_code_style/current_style.md | 15 +++------------ docs/usage_and_configuration/the_basics.md | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 2a5e10162f2..00bd81416dc 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -8,18 +8,9 @@ deliberately limited and rarely added. Previous formatting is taken into account 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 lines that contain -`# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. -`# fmt: skip` can be mixed with other pragmas/comments either with multiple comments -(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separated list (e.g. -`# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation -and in the same block, meaning no unindents beyond the initial indentation level between -them. It also recognizes [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`. +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 diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 73c0d1323e3..eb92887f64f 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -12,7 +12,8 @@ _Black_ is a well-behaved Unix-style command-line tool: ## Usage -To get started right away with sensible defaults: +_Black_ will reformat entire files in place. To get started right away with sensible +defaults: ```sh black {source_file_or_directory} @@ -24,6 +25,17 @@ You can run _Black_ as a package if running it as a script doesn't work: python -m black {source_file_or_directory} ``` +### Ignoring sections + +Black will not reformat lines that contain `# fmt: skip` or blocks that start with +`# fmt: off` and end with `# fmt: on`. `# fmt: skip` can be mixed with other +pragmas/comments either with multiple comments (e.g. `# fmt: skip # pylint # noqa`) or +as a semicolon separated list (e.g. `# fmt: skip; pylint; noqa`). `# fmt: on/off` must +be on the same level of indentation and in the same block, meaning no unindents beyond +the initial indentation level between them. Black also recognizes +[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a +courtesy for straddling code. + ### Command line options The CLI options of _Black_ can be displayed by running `black --help`. All options are @@ -191,7 +203,7 @@ All done! ✨ 🍰 ✨ Show (or do not show) colored diff. Only applies when `--diff` is given. -### `--line-ranges` +#### `--line-ranges` When specified, _Black_ will try its best to only format these lines. From 61b529b7d15400309379f36104885a1dfcd2d026 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 18:29:09 -0800 Subject: [PATCH 657/700] Allow empty lines at beginning of blocks (again) (#4060) --- CHANGES.md | 2 ++ src/black/lines.py | 14 +++++------- src/black/mode.py | 2 +- ...s.py => preview_allow_empty_first_line.py} | 22 +++++++++++++++++++ tests/data/cases/preview_form_feeds.py | 1 + 5 files changed, 31 insertions(+), 10 deletions(-) rename tests/data/cases/{preview_allow_empty_first_line_in_special_cases.py => preview_allow_empty_first_line.py} (87%) diff --git a/CHANGES.md b/CHANGES.md index 8f0b75e7f10..fa0d2494f67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) +- Allow empty lines at the beginning of all blocks, except immediately before a + docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 6e33ee57eab..4050f819757 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -689,18 +689,14 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 + # In preview mode, always allow blank lines, except right before a function + # docstring is_empty_first_line_ok = ( - Preview.allow_empty_first_line_before_new_block_or_comment - in current_line.mode + Preview.allow_empty_first_line_in_block in current_line.mode and ( - # If it's a standalone comment - current_line.leaves[0].type == STANDALONE_COMMENT - # If it opens a new block - or current_line.opens_block - # If it's a triple quote comment (but not at the start of a funcdef) + not is_docstring(current_line.leaves[0]) or ( - is_docstring(current_line.leaves[0]) - and self.previous_line + self.previous_line and self.previous_line.leaves[0] and self.previous_line.leaves[0].parent and not is_funcdef(self.previous_line.leaves[0].parent) diff --git a/src/black/mode.py b/src/black/mode.py index 04038f49627..9df19618363 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -191,7 +191,7 @@ class Preview(Enum): accept_raw_docstrings = auto() fix_power_op_line_length = auto() hug_parens_with_braces_and_square_brackets = auto() - allow_empty_first_line_before_new_block_or_comment = auto() + allow_empty_first_line_in_block = auto() single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() diff --git a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py b/tests/data/cases/preview_allow_empty_first_line.py similarity index 87% rename from tests/data/cases/preview_allow_empty_first_line_in_special_cases.py rename to tests/data/cases/preview_allow_empty_first_line.py index 96c1433c110..3e14fa15250 100644 --- a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py +++ b/tests/data/cases/preview_allow_empty_first_line.py @@ -51,6 +51,17 @@ def baz(): if x: a = 123 +def quux(): + + new_line = here + + +class Cls: + + def method(self): + + pass + # output def foo(): @@ -104,3 +115,14 @@ def baz(): # OK if x: a = 123 + + +def quux(): + + new_line = here + + +class Cls: + def method(self): + + pass diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py index 2d8653a1f04..c236f177a95 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/preview_form_feeds.py @@ -198,6 +198,7 @@ def foo(): # form feeds are prohibited inside blocks, or on a line with nonwhitespace def bar(a=1, b: bool = False): + pass From ce28be2705ab29f184ec4a00aa3d23340630796d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 21:14:25 -0800 Subject: [PATCH 658/700] Add dedicated preview feature for East Asian Width (#4097) --- src/black/lines.py | 2 +- src/black/mode.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/lines.py b/src/black/lines.py index 4050f819757..2a41db173d4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -851,7 +851,7 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) - width = str_width if mode.preview else len + width = str_width if Preview.respect_east_asian_width in mode else len if Preview.multiline_string_handling not in mode: return ( diff --git a/src/black/mode.py b/src/black/mode.py index 9df19618363..38b861e39ca 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,6 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() + respect_east_asian_width = auto() class Deprecated(UserWarning): From 67b23d71854c19921cc6092c695d3301ab99229c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:32:04 -0800 Subject: [PATCH 659/700] Bump actions/setup-python from 4 to 5 (#4101) --- .github/workflows/diff_shades.yml | 4 ++-- .github/workflows/diff_shades_comment.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pypi_upload.yml | 2 +- .github/workflows/release_tests.yml | 2 +- .github/workflows/test.yml | 4 ++-- .github/workflows/upload_binary.yml | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 6bfc6ca9ed8..8d8be2550b0 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -57,7 +57,7 @@ jobs: # The baseline revision could be rather old so a full clone is ideal. fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 49fd376d85e..9b3b4b579da 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fa3d87c70f5..006991a16d8 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 48c26452c54..42a399fd0aa 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9c7aca8f869..2d016cef7a6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: fi - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index bbdcdf17a8f..8e3eb67a10d 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index 74729445052..192ba004f81 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -34,7 +34,7 @@ jobs: # Give us all history, branches and tags fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f8928cc42a..55359a23303 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -96,7 +96,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index bb19d48158c..06e55cfe93a 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" From 9aea9768cb60d23f2f4d331e94c4ee07ef1683a5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:19:02 -0800 Subject: [PATCH 660/700] Only use dummy implementation logic for functions and classes (#4066) Fixes #4063 --- CHANGES.md | 2 ++ src/black/linegen.py | 4 ++-- src/black/nodes.py | 9 +++++++- .../cases/preview_dummy_implementations.py | 22 +++++++++++++++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa0d2494f67..62caea41c31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ - Allow empty lines at the beginning of all blocks, except immediately before a docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) +- Keep suites consisting of only an ellipsis on their own lines if they are not + functions or class definitions (#4066) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 073672a5ae7..6934823d340 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -286,7 +286,7 @@ def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" if ( self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_suite(node): + ) and is_stub_suite(node, self.mode): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -314,7 +314,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: if ( not (self.mode.is_pyi or Preview.dummy_implementations in self.mode) or not node.parent - or not is_stub_suite(node.parent) + or not is_stub_suite(node.parent, self.mode) ): yield from self.line() yield from self.visit_default(node) diff --git a/src/black/nodes.py b/src/black/nodes.py index de53f8e36a3..9b8d9a97835 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -736,8 +736,15 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef -def is_stub_suite(node: Node) -> bool: +def is_stub_suite(node: Node, mode: Mode) -> bool: """Return True if `node` is a suite with a stub body.""" + if node.parent is not None: + if Preview.dummy_implementations in mode and node.parent.type not in ( + syms.funcdef, + syms.async_funcdef, + syms.classdef, + ): + return False # If there is a comment, we want to keep it. if node.prefix.strip(): diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/preview_dummy_implementations.py index 98b69bf87b2..113ac36cdc5 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/preview_dummy_implementations.py @@ -1,9 +1,11 @@ # flags: --preview from typing import NoReturn, Protocol, Union, overload +class Empty: + ... def dummy(a): ... -def other(b): ... +async def other(b): ... @overload @@ -48,13 +50,22 @@ def b(arg: Union[int, str, object]) -> Union[int, str]: raise TypeError return arg +def has_comment(): + ... # still a dummy + +if some_condition: + ... + # output from typing import NoReturn, Protocol, Union, overload +class Empty: ... + + def dummy(a): ... -def other(b): ... +async def other(b): ... @overload @@ -98,3 +109,10 @@ def b(arg: Union[int, str, object]) -> Union[int, str]: if not isinstance(arg, (int, str)): raise TypeError return arg + + +def has_comment(): ... # still a dummy + + +if some_condition: + ... From 0c9899956d890a9dc9c3adbc80b478a47846ced9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 14:29:33 -0800 Subject: [PATCH 661/700] Fix path in test message (#4102) --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index 899cbeb111d..23815da9042 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -317,7 +317,7 @@ def test_expression_diff(self) -> None: msg = ( "Expected diff isn't equal to the actual. If you made changes to" " expression.py and this is an anticipated difference, overwrite" - f" tests/data/expression.diff with {dump}" + f" tests/data/cases/expression.diff with {dump}" ) self.assertEqual(expected, actual, msg) From eb7661f8ab9bff344835693c7c08789bb195137e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 14:41:41 -0800 Subject: [PATCH 662/700] Fix another case where we format dummy implementation for non-functions/classes (#4103) --- CHANGES.md | 2 +- src/black/linegen.py | 12 +++++++----- src/black/nodes.py | 17 ++++++++++------- .../data/cases/preview_dummy_implementations.py | 5 +++++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 62caea41c31..dcf6613b70c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,7 +21,7 @@ docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) - Keep suites consisting of only an ellipsis on their own lines if they are not - functions or class definitions (#4066) + functions or class definitions (#4066) (#4103) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 6934823d340..245be235231 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -42,6 +42,7 @@ is_atom_with_invisible_parens, is_docstring, is_empty_tuple, + is_function_or_class, is_lpar_token, is_multiline_string, is_name_token, @@ -299,11 +300,12 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: 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 or Preview.dummy_implementations in self.mode - ) and is_stub_body(node): + if node.parent and node.parent.type in STATEMENT: + if Preview.dummy_implementations in self.mode: + condition = is_function_or_class(node.parent) + else: + condition = self.mode.is_pyi + if condition and is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) diff --git a/src/black/nodes.py b/src/black/nodes.py index 9b8d9a97835..a4f555b4032 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -736,15 +736,18 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef +def is_function_or_class(node: Node) -> bool: + return node.type in {syms.funcdef, syms.classdef, syms.async_funcdef} + + def is_stub_suite(node: Node, mode: Mode) -> bool: """Return True if `node` is a suite with a stub body.""" - if node.parent is not None: - if Preview.dummy_implementations in mode and node.parent.type not in ( - syms.funcdef, - syms.async_funcdef, - syms.classdef, - ): - return False + if ( + node.parent is not None + and Preview.dummy_implementations in mode + and not is_function_or_class(node.parent) + ): + return False # If there is a comment, we want to keep it. if node.prefix.strip(): diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/preview_dummy_implementations.py index 113ac36cdc5..28b23bb8609 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/preview_dummy_implementations.py @@ -56,6 +56,8 @@ def has_comment(): if some_condition: ... +if already_dummy: ... + # output from typing import NoReturn, Protocol, Union, overload @@ -116,3 +118,6 @@ def has_comment(): ... # still a dummy if some_condition: ... + +if already_dummy: + ... From ebd543c0ac9b8a5f17636d0a42c425e5f693860e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 21:37:15 -0800 Subject: [PATCH 663/700] Fix feature detection for parenthesized context managers (#4104) --- CHANGES.md | 1 + src/black/__init__.py | 18 ++- tests/data/cases/pep_572_remove_parens.py | 2 +- tests/test_black.py | 130 ++++++++++++---------- 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dcf6613b70c..e3b5b7392b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Fix bug where `# fmt: off` automatically dedents when used with the `--line-ranges` option, even when it is not within the specified line range. (#4084) +- Fix feature detection for parenthesized context managers (#4104) ### Preview style diff --git a/src/black/__init__.py b/src/black/__init__.py index 5073fa748d5..735ba713b8f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1351,7 +1351,7 @@ def get_features_used( # noqa: C901 if ( len(atom_children) == 3 and atom_children[0].type == token.LPAR - and atom_children[1].type == syms.testlist_gexp + and _contains_asexpr(atom_children[1]) and atom_children[2].type == token.RPAR ): features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) @@ -1384,6 +1384,22 @@ def get_features_used( # noqa: C901 return features +def _contains_asexpr(node: Union[Node, Leaf]) -> bool: + """Return True if `node` contains an as-pattern.""" + if node.type == syms.asexpr_test: + return True + elif node.type == syms.atom: + if ( + len(node.children) == 3 + and node.children[0].type == token.LPAR + and node.children[2].type == token.RPAR + ): + return _contains_asexpr(node.children[1]) + elif node.type == syms.testlist_gexp: + return any(_contains_asexpr(child) for child in node.children) + return False + + def detect_target_versions( node: Node, *, future_imports: Optional[Set[str]] = None ) -> Set[TargetVersion]: diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 88774d81649..24f1ac29168 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -1,4 +1,4 @@ -# flags: --minimum-version=3.8 --no-preview-line-length-1 +# flags: --minimum-version=3.8 if (foo := 0): pass diff --git a/tests/test_black.py b/tests/test_black.py index 23815da9042..0af5fd2a1f4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -25,6 +25,7 @@ List, Optional, Sequence, + Set, Type, TypeVar, Union, @@ -874,71 +875,88 @@ def test_get_features_used_decorator(self) -> None: ) def test_get_features_used(self) -> None: - node = black.lib2to3_parse("def f(*, arg): ...\n") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def f(*, arg,): ...\n") - self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF}) - node = black.lib2to3_parse("f(*arg,)\n") - self.assertEqual( - black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL} + self.check_features_used("def f(*, arg): ...\n", set()) + self.check_features_used( + "def f(*, arg,): ...\n", {Feature.TRAILING_COMMA_IN_DEF} ) - node = black.lib2to3_parse("def f(*, arg): f'string'\n") - self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS}) - node = black.lib2to3_parse("123_456\n") - self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) - node = black.lib2to3_parse("123456\n") - self.assertEqual(black.get_features_used(node), set()) + self.check_features_used("f(*arg,)\n", {Feature.TRAILING_COMMA_IN_CALL}) + self.check_features_used("def f(*, arg): f'string'\n", {Feature.F_STRINGS}) + self.check_features_used("123_456\n", {Feature.NUMERIC_UNDERSCORES}) + self.check_features_used("123456\n", set()) + source, expected = read_data("cases", "function") - node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.F_STRINGS, } - self.assertEqual(black.get_features_used(node), expected_features) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), expected_features) + self.check_features_used(source, expected_features) + self.check_features_used(expected, expected_features) + source, expected = read_data("cases", "expression") - node = black.lib2to3_parse(source) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("lambda a, /, b: ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(a, /, b): ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(): yield a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): return a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): yield *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("def fn(): return a, *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("x = a, *b, c") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = regular") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = (regular, regular)") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c") - self.assertEqual( - black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} + self.check_features_used(source, set()) + self.check_features_used(expected, set()) + + self.check_features_used("lambda a, /, b: ...\n", {Feature.POS_ONLY_ARGUMENTS}) + self.check_features_used("def fn(a, /, b): ...", {Feature.POS_ONLY_ARGUMENTS}) + + self.check_features_used("def fn(): yield a, b", set()) + self.check_features_used("def fn(): return a, b", set()) + self.check_features_used("def fn(): yield *b, c", {Feature.UNPACKING_ON_FLOW}) + self.check_features_used( + "def fn(): return a, *b, c", {Feature.UNPACKING_ON_FLOW} ) - node = black.lib2to3_parse("try: pass\nexcept Something: pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept *Group: pass") - self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) - node = black.lib2to3_parse("a[*b]") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("a[x, *y(), z] = t") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("def fn(*args: *T): pass") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + self.check_features_used("x = a, *b, c", set()) + + self.check_features_used("x: Any = regular", set()) + self.check_features_used("x: Any = (regular, regular)", set()) + self.check_features_used("x: Any = Complex(Type(1))[something]", set()) + self.check_features_used( + "x: Tuple[int, ...] = a, b, c", {Feature.ANN_ASSIGN_EXTENDED_RHS} + ) + + self.check_features_used("try: pass\nexcept Something: pass", set()) + self.check_features_used("try: pass\nexcept (*Something,): pass", set()) + self.check_features_used( + "try: pass\nexcept *Group: pass", {Feature.EXCEPT_STAR} + ) + + self.check_features_used("a[*b]", {Feature.VARIADIC_GENERICS}) + self.check_features_used("a[x, *y(), z] = t", {Feature.VARIADIC_GENERICS}) + self.check_features_used("def fn(*args: *T): pass", {Feature.VARIADIC_GENERICS}) + + self.check_features_used("with a: pass", set()) + self.check_features_used("with a, b: pass", set()) + self.check_features_used("with a as b: pass", set()) + self.check_features_used("with a as b, c as d: pass", set()) + self.check_features_used("with (a): pass", set()) + self.check_features_used("with (a, b): pass", set()) + self.check_features_used("with (a, b) as (c, d): pass", set()) + self.check_features_used( + "with (a as b): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a as b)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, b as c): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, (b as c)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a, ((b as c)))): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + + def check_features_used(self, source: str, expected: Set[Feature]) -> None: + node = black.lib2to3_parse(source) + actual = black.get_features_used(node) + msg = f"Expected {expected} but got {actual} for {source!r}" + try: + self.assertEqual(actual, expected, msg=msg) + except AssertionError: + DebugVisitor.show(node) + raise def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From d9ad09a32b0e0481bb4fef548d35b7a49cc03c5d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 21:55:28 -0800 Subject: [PATCH 664/700] Prepare release 23.12.0 (#4105) --- CHANGES.md | 33 +++++---------------- docs/integrations/source_version_control.md | 4 +-- docs/usage_and_configuration/the_basics.md | 6 ++-- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e3b5b7392b9..223d7d2c819 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,16 @@ # Change Log -## Unreleased +## 23.12.0 ### Highlights - +It's almost 2024, which means it's time for a new edition of _Black_'s stable style! +Together with this release, we'll put out an alpha release 24.1a1 showcasing the draft +2024 stable style, which we'll finalize in the January release. Please try it out and +[share your feedback](https://github.com/psf/black/issues/4042). + +This release (23.12.0) will still produce the 2023 style. Most but not all of the +changes in `--preview` mode will be in the 2024 stable style. ### Stable style @@ -26,8 +32,6 @@ ### Configuration - - - `--line-ranges` now skips _Black_'s internal stability check in `--safe` mode. This avoids a crash on rare inputs that have many unformatted same-content lines. (#4034) @@ -36,33 +40,12 @@ - Upgrade to mypy 1.7.1 (#4049) (#4069) - Faster compiled wheels are now available for CPython 3.12 (#4070) -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - ### Integrations - Enable 3.12 CI (#4035) - Build docker images in parallel (#4054) - Build docker images with 3.12 (#4055) -### Documentation - - - ## 23.11.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 3c7ef89918f..ca810f1d8f6 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black-jupyter # 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 eb92887f64f..2dbb573803c 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,8 +241,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.11.0 (compiled: yes) -$ black --required-version 23.11.0 -c "format = 'this'" +black, 23.12.0 (compiled: yes) +$ black --required-version 23.12.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -333,7 +333,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.11.0 +black, 23.12.0 ``` #### `--config` From 35ce37ded7bd8fdd3950af19e7c11f311ee7b8d8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 22:28:46 -0800 Subject: [PATCH 665/700] Add new changelog template --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 223d7d2c819..9d79b0fb61a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.12.0 ### Highlights From 8fec1c30855890cc9cfce5ae6d633a1c1a21d724 Mon Sep 17 00:00:00 2001 From: Bryce Willey Date: Thu, 14 Dec 2023 03:28:28 -0500 Subject: [PATCH 666/700] Adds paren to deps for hidden extra constraint (#4108) Fix #4107 --- CHANGES.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9d79b0fb61a..69fe34a5052 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- Fixed a bug that included dependencies from the `d` extra by default (#4108) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index 1098412981a..24b9c07674d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ preview = true # NOTE: You don't need this in your own Black configuration. [build-system] -requires = ["hatchling>=1.8.0", "hatch-vcs", "hatch-fancy-pypi-readme"] +requires = ["hatchling>=1.20.0", "hatch-vcs", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] @@ -187,7 +187,7 @@ CC = "clang" build-frontend = { name = "build", args = ["--no-isolation"] } # Unfortunately, hatch doesn't respect MACOSX_DEPLOYMENT_TARGET before-build = [ - "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.7.1' 'click==8.1.3'", + "python -m pip install 'hatchling==1.20.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.7.1' 'click==8.1.3'", """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, ] From ec91a2be3c44d88e1a3960a4937ad6ed3b63464e Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Fri, 22 Dec 2023 17:04:32 -0600 Subject: [PATCH 667/700] Prepare release 23.12.1 (#4124) --- CHANGES.md | 45 +-------------------- docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 69fe34a5052..d0c9e567457 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,54 +1,11 @@ # Change Log -## Unreleased - -### Highlights - - - -### Stable style - - - -### Preview style - - - -### Configuration - - +## 23.12.1 ### Packaging - - - Fixed a bug that included dependencies from the `d` extra by default (#4108) -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - -### Integrations - - - -### Documentation - - - ## 23.12.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index ca810f1d8f6..3b895193941 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black-jupyter # 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 2dbb573803c..4f9856c6a47 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,8 +241,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.12.0 (compiled: yes) -$ black --required-version 23.12.0 -c "format = 'this'" +black, 23.12.1 (compiled: yes) +$ black --required-version 23.12.1 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -333,7 +333,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.12.0 +black, 23.12.1 ``` #### `--config` From 1b831f214a111dfb45a571fc40f4404bb6b5b62c Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Fri, 22 Dec 2023 17:46:06 -0600 Subject: [PATCH 668/700] Add new changelog template (#4125) --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d0c9e567457..526cbd12123 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.12.1 ### Packaging From 51786141cc4eb3c212be76638e66b91648d0e5f8 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Thu, 28 Dec 2023 01:23:42 -0500 Subject: [PATCH 669/700] Fix nits, chain comparisons, unused params, hyphens (#4114) --- src/black/brackets.py | 4 ++-- src/black/cache.py | 4 ++-- src/black/comments.py | 3 +-- src/black/linegen.py | 2 +- src/black/lines.py | 4 ++-- src/black/ranges.py | 5 +---- src/black/trans.py | 21 ++++++++++----------- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/black/brackets.py b/src/black/brackets.py index 3020cc0d390..37e6b2590eb 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -115,7 +115,7 @@ def mark(self, leaf: Leaf) -> None: if delim and self.previous is not None: self.delimiters[id(self.previous)] = delim else: - delim = is_split_after_delimiter(leaf, self.previous) + delim = is_split_after_delimiter(leaf) if delim: self.delimiters[id(leaf)] = delim if leaf.type in OPENING_BRACKETS: @@ -215,7 +215,7 @@ def get_open_lsqb(self) -> Optional[Leaf]: return self.bracket_match.get((self.depth - 1, token.RSQB)) -def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: +def is_split_after_delimiter(leaf: Leaf) -> Priority: """Return the priority of the `leaf` delimiter, given a line break after it. The delimiter priorities returned here are from those delimiters that would diff --git a/src/black/cache.py b/src/black/cache.py index 6baa096baca..c844c37b6f8 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -58,9 +58,9 @@ class Cache: @classmethod def read(cls, mode: Mode) -> Self: - """Read the cache if it exists and is well formed. + """Read the cache if it exists and is well-formed. - If it is not well formed, the call to write later should + If it is not well-formed, the call to write later should resolve the issue. """ cache_file = get_cache_file(mode) diff --git a/src/black/comments.py b/src/black/comments.py index 25413121199..52bb024a799 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -221,8 +221,7 @@ def convert_one_fmt_off_pair( if comment.value in FMT_OFF: fmt_off_prefix = "" if len(lines) > 0 and not any( - comment_lineno >= line[0] and comment_lineno <= line[1] - for line in lines + line[0] <= comment_lineno <= line[1] for line in lines ): # keeping indentation of comment by preserving original whitespaces. fmt_off_prefix = prefix.split(comment.value)[0] diff --git a/src/black/linegen.py b/src/black/linegen.py index 245be235231..0fd4a8d9c96 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1635,7 +1635,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf opening_bracket: Optional[Leaf] = None closing_bracket: Optional[Leaf] = None inner_brackets: Set[LeafID] = set() - for index, leaf, leaf_length in line.enumerate_with_length(reversed=True): + for index, leaf, leaf_length in line.enumerate_with_length(is_reversed=True): length += leaf_length if length > line_length: break diff --git a/src/black/lines.py b/src/black/lines.py index 2a41db173d4..0cd4189a778 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -451,7 +451,7 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: ) def enumerate_with_length( - self, reversed: bool = False + self, is_reversed: bool = False ) -> Iterator[Tuple[Index, Leaf, int]]: """Return an enumeration of leaves with their length. @@ -459,7 +459,7 @@ def enumerate_with_length( """ op = cast( Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]], - enumerate_reversed if reversed else enumerate, + enumerate_reversed if is_reversed else enumerate, ) for index, leaf in op(self.leaves): length = len(leaf.prefix) + len(leaf.value) diff --git a/src/black/ranges.py b/src/black/ranges.py index 59e19242d47..06fa8790554 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -487,10 +487,7 @@ def _find_lines_mapping_index( index = start_index while index < len(lines_mappings): mapping = lines_mappings[index] - if ( - mapping.original_start <= original_line - and original_line <= mapping.original_end - ): + if mapping.original_start <= original_line <= mapping.original_end: return index index += 1 return index diff --git a/src/black/trans.py b/src/black/trans.py index ab3197fa6df..7c7335a005b 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1273,7 +1273,7 @@ def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]: i += 1 continue - # if we're in an expression part of the f-string, fast forward through strings + # if we're in an expression part of the f-string, fast-forward through strings # note that backslashes are not legal in the expression portion of f-strings if stack: delim = None @@ -1740,7 +1740,7 @@ def passes_all_checks(i: Index) -> bool: """ Returns: True iff ALL of the conditions listed in the 'Transformations' - section of this classes' docstring would be be met by returning @i. + section of this classes' docstring would be met by returning @i. """ is_space = string[i] == " " is_split_safe = is_valid_index(i - 1) and string[i - 1] in SPLIT_SAFE_CHARS @@ -1932,7 +1932,7 @@ def _return_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of a return/yield statement and the first leaf + # If this line is a part of a return/yield statement and the first leaf # contains either the "return" or "yield" keywords... if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[ 0 @@ -1957,7 +1957,7 @@ def _else_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of a ternary expression and the first leaf + # If this line is a part of a ternary expression and the first leaf # contains the "else" keyword... if ( parent_type(LL[0]) == syms.test @@ -1984,7 +1984,7 @@ def _assert_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of an assert statement and the first leaf + # If this line is a part of an assert statement and the first leaf # contains the "assert" keyword... if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert": is_valid_index = is_valid_index_factory(LL) @@ -2019,7 +2019,7 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of an expression statement or is a function + # If this line is a part of an expression statement or is a function # argument AND the first leaf contains a variable name... if ( parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power] @@ -2040,7 +2040,7 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: string_parser = StringParser() idx = string_parser.parse(LL, string_idx) - # The next leaf MAY be a comma iff this line is apart + # The next leaf MAY be a comma iff this line is a part # of a function argument... if ( parent_type(LL[0]) == syms.argument @@ -2187,8 +2187,7 @@ def do_transform( if opening_bracket is not None and opening_bracket in left_leaves: index = left_leaves.index(opening_bracket) if ( - index > 0 - and index < len(left_leaves) - 1 + 0 < index < len(left_leaves) - 1 and left_leaves[index - 1].type == token.COLON and left_leaves[index + 1].value == "lambda" ): @@ -2297,7 +2296,7 @@ def parse(self, leaves: List[Leaf], string_idx: int) -> int: * @leaves[@string_idx].type == token.STRING Returns: - The index directly after the last leaf which is apart of the string + The index directly after the last leaf which is a part of the string trailer, if a "trailer" exists. OR @string_idx + 1, if no string "trailer" exists. @@ -2320,7 +2319,7 @@ def _next_state(self, leaf: Leaf) -> bool: MUST be the leaf directly following @leaf. Returns: - True iff @leaf is apart of the string's trailer. + True iff @leaf is a part of the string's trailer. """ # We ignore empty LPAR or RPAR leaves. if is_empty_par(leaf): From c80685f36183f146f831a5737510cf105f947745 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:24:25 -0600 Subject: [PATCH 670/700] Treat walruses like other binary operators in subscripts (#4109) Fixes #4078 --- CHANGES.md | 3 +++ src/black/lines.py | 9 ++++++++- tests/data/cases/preview_pep_572.py | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 526cbd12123..1444463050f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Fix bug where spaces were not added around parenthesized walruses in subscripts, + unlike other binary operators (#4109) + ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 0cd4189a778..d153b8c2e1b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -446,8 +446,15 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: if subscript_start.type == syms.subscriptlist: subscript_start = child_towards(subscript_start, leaf) + + # When this is moved out of preview, add syms.namedexpr_test directly to + # TEST_DESCENDANTS in nodes.py + if Preview.walrus_subscript in self.mode: + test_decendants = TEST_DESCENDANTS | {syms.namedexpr_test} + else: + test_decendants = TEST_DESCENDANTS return subscript_start is not None and any( - n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() + n.type in test_decendants for n in subscript_start.pre_order() ) def enumerate_with_length( diff --git a/tests/data/cases/preview_pep_572.py b/tests/data/cases/preview_pep_572.py index 8e801ff6cdc..75ad0cc4176 100644 --- a/tests/data/cases/preview_pep_572.py +++ b/tests/data/cases/preview_pep_572.py @@ -3,5 +3,5 @@ x[:(a:=0)] # output -x[(a := 0):] -x[:(a := 0)] +x[(a := 0) :] +x[: (a := 0)] From bf6cabc8049cbdf4d0b8af33134317a0190a614f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:24:57 -0800 Subject: [PATCH 671/700] Do not round cache mtimes (#4128) Fixes #4116 This logic was introduced in #3821, I believe as a result of copying logic inside mypy that I think isn't relevant to Black --- CHANGES.md | 2 ++ src/black/cache.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1444463050f..2389f6d39fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ +- Fix cache mtime logic that resulted in false positive cache hits (#4128) + ### Packaging diff --git a/src/black/cache.py b/src/black/cache.py index c844c37b6f8..cfdbc21e92a 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -101,7 +101,7 @@ def is_changed(self, source: Path) -> bool: st = res_src.stat() if st.st_size != old.st_size: return True - if int(st.st_mtime) != int(old.st_mtime): + if st.st_mtime != old.st_mtime: new_hash = Cache.hash_digest(res_src) if new_hash != old.hash: return True From db9c592967b976a16eccd500f3e2676cfff7f29d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 27 Dec 2023 22:59:30 -0800 Subject: [PATCH 672/700] Unify docstring detection (#4095) Co-authored-by: hauntsaninja --- CHANGES.md | 1 + src/black/linegen.py | 4 ++-- src/black/lines.py | 15 +++++++++++---- src/black/mode.py | 1 + src/black/nodes.py | 12 +++++++++++- src/black/strings.py | 2 ++ tests/data/cases/module_docstring_2.py | 2 ++ .../preview_no_blank_line_before_docstring.py | 7 +++++++ 8 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2389f6d39fd..a6587cc5ceb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ +- Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) diff --git a/src/black/linegen.py b/src/black/linegen.py index 0fd4a8d9c96..0972cf432e1 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -424,7 +424,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) - if is_docstring(leaf) and not re.search(r"\\\s*\n", leaf.value): + if is_docstring(leaf, self.mode) and not re.search(r"\\\s*\n", leaf.value): # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. if self.mode.string_normalization: @@ -477,7 +477,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote = quote_char * quote_len # It's invalid to put closing single-character quotes on a new line. - if self.mode and quote_len == 3: + if quote_len == 3: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. diff --git a/src/black/lines.py b/src/black/lines.py index d153b8c2e1b..8d02267a85b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -196,7 +196,7 @@ def is_class_paren_empty(self) -> bool: ) @property - def is_triple_quoted_string(self) -> bool: + def _is_triple_quoted_string(self) -> bool: """Is the line a triple quoted string?""" if not self or self.leaves[0].type != token.STRING: return False @@ -209,6 +209,13 @@ def is_triple_quoted_string(self) -> bool: return True return False + @property + def is_docstring(self) -> bool: + """Is the line a docstring?""" + if Preview.unify_docstring_detection not in self.mode: + return self._is_triple_quoted_string + return bool(self) and is_docstring(self.leaves[0], self.mode) + @property def is_chained_assignment(self) -> bool: """Is the line a chained assignment""" @@ -583,7 +590,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and self.previous_block and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 - and self.previous_block.original_line.is_triple_quoted_string + and self.previous_block.original_line.is_docstring and not (current_line.is_class or current_line.is_def) ): before = 1 @@ -690,7 +697,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if ( self.previous_line and self.previous_line.is_class - and current_line.is_triple_quoted_string + and current_line.is_docstring ): if Preview.no_blank_line_before_class_docstring in current_line.mode: return 0, 1 @@ -701,7 +708,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: is_empty_first_line_ok = ( Preview.allow_empty_first_line_in_block in current_line.mode and ( - not is_docstring(current_line.leaves[0]) + not is_docstring(current_line.leaves[0], current_line.mode) or ( self.previous_line and self.previous_line.leaves[0] diff --git a/src/black/mode.py b/src/black/mode.py index 38b861e39ca..466b78228fc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,6 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() + unify_docstring_detection = auto() respect_east_asian_width = auto() diff --git a/src/black/nodes.py b/src/black/nodes.py index a4f555b4032..8e0f27e3ded 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -531,7 +531,7 @@ def is_arith_like(node: LN) -> bool: } -def is_docstring(leaf: Leaf) -> bool: +def is_docstring(leaf: Leaf, mode: Mode) -> bool: if leaf.type != token.STRING: return False @@ -539,6 +539,16 @@ def is_docstring(leaf: Leaf) -> bool: if set(prefix).intersection("bBfF"): return False + if ( + Preview.unify_docstring_detection in mode + and leaf.parent + and leaf.parent.type == syms.simple_stmt + and not leaf.parent.prev_sibling + and leaf.parent.parent + and leaf.parent.parent.type == syms.file_input + ): + return True + if prev_siblings_are( leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] ): diff --git a/src/black/strings.py b/src/black/strings.py index 0d30f09ed11..0e0f968824b 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -63,6 +63,8 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: ) else: lines.append(line) + if s.endswith("\n"): + lines.append("") return lines diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index e1f81b4d76b..1cc9aea9aea 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -1,6 +1,7 @@ # flags: --preview """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, @@ -38,6 +39,7 @@ # output """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, diff --git a/tests/data/cases/preview_no_blank_line_before_docstring.py b/tests/data/cases/preview_no_blank_line_before_docstring.py index 303035a7efb..faeaa1e46e4 100644 --- a/tests/data/cases/preview_no_blank_line_before_docstring.py +++ b/tests/data/cases/preview_no_blank_line_before_docstring.py @@ -29,6 +29,9 @@ class MultilineDocstringsAsWell: and on so many lines... """ +class SingleQuotedDocstring: + + "I'm a docstring but I don't even get triple quotes." # output @@ -57,3 +60,7 @@ class MultilineDocstringsAsWell: and on so many lines... """ + + +class SingleQuotedDocstring: + "I'm a docstring but I don't even get triple quotes." From c35924663cff4f696f9bb91ca9c7775487d95ac6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:12:18 -0800 Subject: [PATCH 673/700] [pre-commit.ci] pre-commit autoupdate (#4139) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2896489d724..13479565527 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: additional_dependencies: *version_check_dependencies - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy exclude: ^docs/conf.py @@ -58,13 +58,13 @@ repos: - hypothesmith - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.8 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace From fe3376141c333271d3c64d7fa0e433652e2b48ff Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:46:09 -0800 Subject: [PATCH 674/700] Allow empty lines at beginnings of more blocks (#4130) Fixes #4043, fixes #619 These include nested functions and methods. I think the nested function case quite clearly improves readability. I think the method case improves consistency, adherence to PEP 8 and resolves a point of contention. --- CHANGES.md | 2 ++ src/black/lines.py | 5 ++++- tests/data/cases/class_blank_parentheses.py | 1 + .../cases/preview_allow_empty_first_line.py | 19 +++++++++++++++++++ tests/data/cases/preview_form_feeds.py | 1 + 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a6587cc5ceb..fca88612afe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) +- Address a missing case in the change to allow empty lines at the beginning of all + blocks, except immediately before a docstring (#4130) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 8d02267a85b..4d4f47a44e8 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -745,7 +745,10 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 if self.previous_line.depth < current_line.depth and ( self.previous_line.is_class or self.previous_line.is_def ): - return 0, 0 + if self.mode.is_pyi or not Preview.allow_empty_first_line_in_block: + return 0, 0 + else: + return 1 if user_had_newline else 0, 0 comment_to_add_newlines: Optional[LinesBlock] = None if ( diff --git a/tests/data/cases/class_blank_parentheses.py b/tests/data/cases/class_blank_parentheses.py index 1a5721a2889..3c460d9bd79 100644 --- a/tests/data/cases/class_blank_parentheses.py +++ b/tests/data/cases/class_blank_parentheses.py @@ -39,6 +39,7 @@ def test_func(self): class ClassWithEmptyFunc(object): + def func_with_blank_parentheses(): return 5 diff --git a/tests/data/cases/preview_allow_empty_first_line.py b/tests/data/cases/preview_allow_empty_first_line.py index 3e14fa15250..daf78344ad7 100644 --- a/tests/data/cases/preview_allow_empty_first_line.py +++ b/tests/data/cases/preview_allow_empty_first_line.py @@ -62,6 +62,15 @@ def method(self): pass + +def top_level( + a: int, + b: str, +) -> Whatever[Generic, Something]: + + def nested(x: int) -> int: + pass + # output def foo(): @@ -123,6 +132,16 @@ def quux(): class Cls: + def method(self): pass + + +def top_level( + a: int, + b: str, +) -> Whatever[Generic, Something]: + + def nested(x: int) -> int: + pass diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py index c236f177a95..dc3bd6cfe2e 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/preview_form_feeds.py @@ -203,6 +203,7 @@ def bar(a=1, b: bool = False): class Baz: + def __init__(self): pass From b9ad4da2e81f6ec66d292b85f284889211e052b4 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 16:55:25 -0800 Subject: [PATCH 675/700] Revert "confine pre-commit to stages (#3940)" (#4137) This reverts commit 7686989fc89aad5ea235a34977ebf8c81c26c4eb. --- .pre-commit-hooks.yaml | 2 -- CHANGES.md | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 54a03efe7a1..a1ff41fded8 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,7 +4,6 @@ name: black description: "Black: The uncompromising Python code formatter" entry: black - stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true @@ -14,7 +13,6 @@ description: "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" entry: black - stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true diff --git a/CHANGES.md b/CHANGES.md index fca88612afe..360319ac964 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,9 @@ +- Revert the change to run Black's pre-commit integration only on specific git hooks + (#3940) for better compatibility with older versions of pre-commit (#4137) + ### Documentation +- Fix comment handling when parenthesising conditional expressions (#4134) - Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) diff --git a/src/black/linegen.py b/src/black/linegen.py index 574c89b880c..4d468ce0f2e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -170,8 +170,12 @@ def visit_test(self, node: Node) -> Iterator[Line]: ) if not already_parenthesized: + # Similar to logic in wrap_in_parentheses lpar = Leaf(token.LPAR, "") rpar = Leaf(token.RPAR, "") + prefix = node.prefix + node.prefix = "" + lpar.prefix = prefix node.insert_child(0, lpar) node.append_child(rpar) diff --git a/tests/data/cases/conditional_expression.py b/tests/data/cases/conditional_expression.py index c30cd76c791..76251bd9318 100644 --- a/tests/data/cases/conditional_expression.py +++ b/tests/data/cases/conditional_expression.py @@ -67,6 +67,28 @@ def something(): else ValuesListIterable ) + +def foo(wait: bool = True): + # This comment is two + # lines long + + # This is only one + time.sleep(1) if wait else None + time.sleep(1) if wait else None + + # With newline above + time.sleep(1) if wait else None + # Without newline above + time.sleep(1) if wait else None + + +a = "".join( + ( + "", # comment + "" if True else "", + ) +) + # output long_kwargs_single_line = my_function( @@ -159,3 +181,23 @@ def something(): if named else FlatValuesListIterable if flat else ValuesListIterable ) + + +def foo(wait: bool = True): + # This comment is two + # lines long + + # This is only one + time.sleep(1) if wait else None + time.sleep(1) if wait else None + + # With newline above + time.sleep(1) if wait else None + # Without newline above + time.sleep(1) if wait else None + + +a = "".join(( + "", # comment + "" if True else "", +)) From e11eaf2f44d3db5713fb99bdec966ba974b60c8c Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 20:14:57 -0800 Subject: [PATCH 680/700] Make `blank_line_after_nested_stub_class` work for methods (#4141) Fixes #4113 Authored by dhruvmanila --- CHANGES.md | 1 + src/black/lines.py | 8 ++++---- tests/data/cases/nested_stub.py | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3dc0c87f89a..8fb8677dd77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Remove empty lines before docstrings in async functions (#4132) - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) +- For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index b544c5e0035..9eb5785da57 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -640,15 +640,15 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if previous_def is not None: assert self.previous_line is not None if self.mode.is_pyi: - if depth and not current_line.is_def and self.previous_line.is_def: - # Empty lines between attributes and methods should be preserved. - before = 1 if user_had_newline else 0 - elif ( + if ( Preview.blank_line_after_nested_stub_class in self.mode and previous_def.is_class and not previous_def.is_stub_class ): before = 1 + elif depth and not current_line.is_def and self.previous_line.is_def: + # Empty lines between attributes and methods should be preserved. + before = 1 if user_had_newline else 0 elif depth: before = 0 else: diff --git a/tests/data/cases/nested_stub.py b/tests/data/cases/nested_stub.py index b81549ec115..ef13c588ce6 100644 --- a/tests/data/cases/nested_stub.py +++ b/tests/data/cases/nested_stub.py @@ -18,6 +18,18 @@ def function_definition(self): ... assignment = 1 def f2(self) -> str: ... + +class TopLevel: + class Nested1: + foo: int + def bar(self): ... + field = 1 + + class Nested2: + def bar(self): ... + foo: int + field = 1 + # output import sys @@ -41,4 +53,17 @@ def f1(self) -> str: ... def function_definition(self): ... assignment = 1 - def f2(self) -> str: ... \ No newline at end of file + def f2(self) -> str: ... + +class TopLevel: + class Nested1: + foo: int + def bar(self): ... + + field = 1 + + class Nested2: + def bar(self): ... + foo: int + + field = 1 From b7c3a9fedd4cfcc6a6a88aacc7b0f599b63d4716 Mon Sep 17 00:00:00 2001 From: Dragorn421 Date: Thu, 11 Jan 2024 16:46:17 +0100 Subject: [PATCH 681/700] Docs: Add note on `--exclude` about possibly verbose regex (#4145) Co-authored-by: Jelle Zijlstra --- docs/usage_and_configuration/the_basics.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 4f9856c6a47..b541f07907c 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -268,6 +268,11 @@ recursive searches. An empty value means no paths are excluded. Use forward slas directories on all platforms (Windows, too). By default, Black also ignores all paths listed in `.gitignore`. Changing this value will override all default exclusions. +If the regular expression contains newlines, it is treated as a +[verbose regular expression](https://docs.python.org/3/library/re.html#re.VERBOSE). This +is typically useful when setting these options in a `pyproject.toml` configuration file; +see [Configuration format](#configuration-format) for more information. + #### `--extend-exclude` Like `--exclude`, but adds additional files and directories on top of the default values From 9a331d606f3fd60cac19bfbfc3f98cbe8be2517d Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:04:15 -0600 Subject: [PATCH 682/700] fix: Don't allow unparenthesizing walruses (#4155) Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com> Signed-off-by: RedGuy12 --- CHANGES.md | 1 + src/black/linegen.py | 6 +++++- tests/data/cases/walrus_in_dict.py | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/walrus_in_dict.py diff --git a/CHANGES.md b/CHANGES.md index 8fb8677dd77..2bd58ed49ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) +- Fix crash when using a walrus in a dictionary (#4155) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 4d468ce0f2e..9a3eb0ce73f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -242,7 +242,11 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if i == 0: continue if node.children[i - 1].type == token.COLON: - if child.type == syms.atom and child.children[0].type == token.LPAR: + if ( + child.type == syms.atom + and child.children[0].type == token.LPAR + and not is_walrus_assignment(child) + ): if maybe_make_parens_invisible_in_atom( child, parent=node, diff --git a/tests/data/cases/walrus_in_dict.py b/tests/data/cases/walrus_in_dict.py new file mode 100644 index 00000000000..c33eecd84a6 --- /dev/null +++ b/tests/data/cases/walrus_in_dict.py @@ -0,0 +1,7 @@ +# flags: --preview +{ + "is_update": (up := commit.hash in update_hashes) +} + +# output +{"is_update": (up := commit.hash in update_hashes)} From 7f60f3dbd7d2d36011fbae6c140b35802932952b Mon Sep 17 00:00:00 2001 From: Kevin Paulson Date: Fri, 19 Jan 2024 18:54:32 -0500 Subject: [PATCH 683/700] Update using_black_with_other_tools.md to ensure flake8 configuration examples are consistant (#4157) --- docs/guides/using_black_with_other_tools.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 22c641a7420..e642a1aef33 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -145,7 +145,7 @@ There are a few deviations that cause incompatibilities with _Black_. ``` max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` #### Why those options above? @@ -184,7 +184,7 @@ extend-ignore = E203, E704 ```ini [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` @@ -195,7 +195,7 @@ extend-ignore = E203 ```ini [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` From 995e4ada14d63a9bec39c5fc83275d0e49742618 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:13:26 -0800 Subject: [PATCH 684/700] Fix unnecessary nesting when wrapping long dict (#4135) Fixes #4129 --- CHANGES.md | 1 + src/black/linegen.py | 7 ++-- tests/data/cases/preview_long_dict_values.py | 38 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2bd58ed49ff..1e75fb58563 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) - Fix crash when using a walrus in a dictionary (#4155) +- Fix unnecessary parentheses when wrapping long dicts (#4135) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 9a3eb0ce73f..dd296eb801d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -244,15 +244,14 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if node.children[i - 1].type == token.COLON: if ( child.type == syms.atom - and child.children[0].type == token.LPAR + and child.children[0].type in OPENING_BRACKETS and not is_walrus_assignment(child) ): - if maybe_make_parens_invisible_in_atom( + maybe_make_parens_invisible_in_atom( child, parent=node, remove_brackets_around_comma=False, - ): - wrap_in_parentheses(node, child, visible=False) + ) else: wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) diff --git a/tests/data/cases/preview_long_dict_values.py b/tests/data/cases/preview_long_dict_values.py index fbbacd13d1d..54da76038dc 100644 --- a/tests/data/cases/preview_long_dict_values.py +++ b/tests/data/cases/preview_long_dict_values.py @@ -37,6 +37,26 @@ } +class Random: + def func(): + random_service.status.active_states.inactive = ( + make_new_top_level_state_from_dict( + { + "topLevelBase": { + "secondaryBase": { + "timestamp": 1234, + "latitude": 1, + "longitude": 2, + "actionTimestamp": Timestamp( + seconds=1530584000, nanos=0 + ).ToJsonString(), + } + }, + } + ) + ) + + # output @@ -89,3 +109,21 @@ } ), } + + +class Random: + def func(): + random_service.status.active_states.inactive = ( + make_new_top_level_state_from_dict({ + "topLevelBase": { + "secondaryBase": { + "timestamp": 1234, + "latitude": 1, + "longitude": 2, + "actionTimestamp": ( + Timestamp(seconds=1530584000, nanos=0).ToJsonString() + ), + } + }, + }) + ) From 6f3fb78444655f883780dcc19349226833c677c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:22:56 -0800 Subject: [PATCH 685/700] Bump actions/cache from 3 to 4 (#4162) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 8d8be2550b0..0e1aab00e34 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -72,7 +72,7 @@ jobs: - name: Attempt to use cached baseline analysis id: baseline-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ matrix.baseline-analysis }} key: ${{ matrix.baseline-cache-key }} From 8fe602b1fa91dc6db682d1dba79a8a7341597271 Mon Sep 17 00:00:00 2001 From: Daniel Krzeminski Date: Mon, 22 Jan 2024 11:46:57 -0600 Subject: [PATCH 686/700] fix pathlib exception handling with symlinks (#4161) Fixes #4077 --- CHANGES.md | 2 ++ src/black/__init__.py | 6 +++++- src/black/files.py | 24 ++++++++++++++++-------- tests/test_black.py | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e75fb58563..f29834a3f7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,8 @@ +- Fix symlink handling, properly catch and ignore symlinks that point outside of root + (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 735ba713b8f..e3cbaab5f1d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -49,6 +49,7 @@ find_user_pyproject_toml, gen_python_files, get_gitignore, + get_root_relative_path, normalize_path_maybe_ignore, parse_pyproject_toml, path_is_excluded, @@ -700,7 +701,10 @@ def get_sources( # Compare the logic here to the logic in `gen_python_files`. if is_stdin or path.is_file(): - root_relative_path = path.absolute().relative_to(root).as_posix() + root_relative_path = get_root_relative_path(path, root, report) + + if root_relative_path is None: + continue root_relative_path = "/" + root_relative_path diff --git a/src/black/files.py b/src/black/files.py index 858303ca1a3..65951efdbe8 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -259,14 +259,7 @@ def normalize_path_maybe_ignore( try: abspath = path if path.is_absolute() else Path.cwd() / path normalized_path = abspath.resolve() - try: - root_relative_path = normalized_path.relative_to(root).as_posix() - except ValueError: - if report: - report.path_ignored( - path, f"is a symbolic link that points outside {root}" - ) - return None + root_relative_path = get_root_relative_path(normalized_path, root, report) except OSError as e: if report: @@ -276,6 +269,21 @@ def normalize_path_maybe_ignore( return root_relative_path +def get_root_relative_path( + path: Path, + root: Path, + report: Optional[Report] = None, +) -> Optional[str]: + """Returns the file path relative to the 'root' directory""" + try: + root_relative_path = path.absolute().relative_to(root).as_posix() + except ValueError: + if report: + report.path_ignored(path, f"is a symbolic link that points outside {root}") + return None + return root_relative_path + + def _path_is_ignored( root_relative_path: str, root: Path, diff --git a/tests/test_black.py b/tests/test_black.py index 0af5fd2a1f4..2b5fab5d28d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2592,6 +2592,20 @@ def test_symlinks(self) -> None: outside_root_symlink.resolve.assert_called_once() ignored_symlink.resolve.assert_not_called() + def test_get_sources_with_stdin_symlink_outside_root( + self, + ) -> None: + path = THIS_DIR / "data" / "include_exclude_tests" + stdin_filename = str(path / "b/exclude/a.py") + outside_root_symlink = Path("/target_directory/a.py") + with patch("pathlib.Path.resolve", return_value=outside_root_symlink): + assert_collected_sources( + root=Path("target_directory/"), + src=["-"], + expected=[], + stdin_filename=stdin_filename, + ) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin(self) -> None: src = ["-"] From 59b9d858a30de56801e84c31f57b53337c61647c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 24 Jan 2024 17:06:14 -0800 Subject: [PATCH 687/700] Create the 2024 stable style (#4106) --- CHANGES.md | 40 +++++++- src/black/comments.py | 32 +++---- src/black/linegen.py | 95 +++++-------------- src/black/lines.py | 63 +++--------- src/black/mode.py | 25 +---- src/black/nodes.py | 14 +-- ...irst_line.py => allow_empty_first_line.py} | 1 - ...{preview_async_stmts.py => async_stmts.py} | 1 - tests/data/cases/comments5.py | 9 +- tests/data/cases/conditional_expression.py | 11 ++- ..._managers_38.py => context_managers_38.py} | 2 +- ..._managers_39.py => context_managers_39.py} | 2 +- ....py => context_managers_autodetect_310.py} | 2 +- ....py => context_managers_autodetect_311.py} | 2 +- ...8.py => context_managers_autodetect_38.py} | 1 - ...9.py => context_managers_autodetect_39.py} | 2 +- ...mentations.py => dummy_implementations.py} | 1 - tests/data/cases/empty_lines.py | 1 + tests/data/cases/fmtonoff.py | 8 +- tests/data/cases/fmtonoff5.py | 3 +- .../{preview_form_feeds.py => form_feeds.py} | 1 - tests/data/cases/function.py | 5 +- ...r_match.py => keep_newline_after_match.py} | 6 ++ .../data/cases/long_strings_flag_disabled.py | 16 ++-- tests/data/cases/module_docstring_1.py | 1 - tests/data/cases/module_docstring_2.py | 4 +- tests/data/cases/module_docstring_3.py | 1 - tests/data/cases/module_docstring_4.py | 1 - .../module_docstring_followed_by_class.py | 1 - .../module_docstring_followed_by_function.py | 1 - tests/data/cases/nested_stub.py | 2 +- ...g.py => no_blank_line_before_docstring.py} | 2 +- ...ching_long.py => pattern_matching_long.py} | 2 +- ....py => pattern_matching_trailing_comma.py} | 2 +- .../cases/pep604_union_types_line_breaks.py | 2 +- tests/data/cases/pep_572_py310.py | 6 +- tests/data/cases/pep_572_remove_parens.py | 8 +- .../{preview_pep_572.py => pep_572_slices.py} | 1 - ...nt_precedence.py => percent_precedence.py} | 5 +- ...op_spacing.py => power_op_spacing_long.py} | 1 - ...refer_rhs_split.py => prefer_rhs_split.py} | 1 - tests/data/cases/py310_pep572.py | 2 +- tests/data/cases/python39.py | 9 +- tests/data/cases/raw_docstring.py | 2 +- ... raw_docstring_no_string_normalization.py} | 2 +- .../remove_newline_after_code_block_open.py | 79 +++++++++------ .../data/cases/return_annotation_brackets.py | 34 +++---- ...ine_format_skip_with_multiple_comments.py} | 1 - ...ew_trailing_comma.py => trailing_comma.py} | 1 - tests/data/cases/walrus_in_dict.py | 2 + 50 files changed, 222 insertions(+), 294 deletions(-) rename tests/data/cases/{preview_allow_empty_first_line.py => allow_empty_first_line.py} (98%) rename tests/data/cases/{preview_async_stmts.py => async_stmts.py} (93%) rename tests/data/cases/{preview_context_managers_38.py => context_managers_38.py} (96%) rename tests/data/cases/{preview_context_managers_39.py => context_managers_39.py} (98%) rename tests/data/cases/{preview_context_managers_autodetect_310.py => context_managers_autodetect_310.py} (93%) rename tests/data/cases/{preview_context_managers_autodetect_311.py => context_managers_autodetect_311.py} (92%) rename tests/data/cases/{preview_context_managers_autodetect_38.py => context_managers_autodetect_38.py} (98%) rename tests/data/cases/{preview_context_managers_autodetect_39.py => context_managers_autodetect_39.py} (93%) rename tests/data/cases/{preview_dummy_implementations.py => dummy_implementations.py} (99%) rename tests/data/cases/{preview_form_feeds.py => form_feeds.py} (99%) rename tests/data/cases/{remove_newline_after_match.py => keep_newline_after_match.py} (98%) rename tests/data/cases/{preview_no_blank_line_before_docstring.py => no_blank_line_before_docstring.py} (98%) rename tests/data/cases/{preview_pattern_matching_long.py => pattern_matching_long.py} (94%) rename tests/data/cases/{preview_pattern_matching_trailing_comma.py => pattern_matching_trailing_comma.py} (92%) rename tests/data/cases/{preview_pep_572.py => pep_572_slices.py} (75%) rename tests/data/cases/{preview_percent_precedence.py => percent_precedence.py} (91%) rename tests/data/cases/{preview_power_op_spacing.py => power_op_spacing_long.py} (99%) rename tests/data/cases/{preview_prefer_rhs_split.py => prefer_rhs_split.py} (99%) rename tests/data/cases/{preview_docstring_no_string_normalization.py => raw_docstring_no_string_normalization.py} (88%) rename tests/data/cases/{preview_single_line_format_skip_with_multiple_comments.py => single_line_format_skip_with_multiple_comments.py} (97%) rename tests/data/cases/{preview_trailing_comma.py => trailing_comma.py} (97%) diff --git a/CHANGES.md b/CHANGES.md index f29834a3f7f..0e2974d706e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,22 +6,54 @@ +This release introduces the new 2024 stable style (#4106), stabilizing the following +changes: + +- Add parentheses around `if`-`else` expressions (#2278) +- Dummy class and function implementations consisting only of `...` are formatted more + compactly (#3796) +- If an assignment statement is too long, we now prefer splitting on the right-hand side + (#3368) +- Hex codes in Unicode escape sequences are now standardized to lowercase (#2916) +- Allow empty first lines at the beginning of most blocks (#3967, #4061) +- Add parentheses around long type annotations (#3899) +- Standardize on a single newline after module docstrings (#3932) +- Fix incorrect magic trailing comma handling in return types (#3916) +- Remove blank lines before class docstrings (#3692) +- Wrap multiple context managers in parentheses if combined in a single `with` statement + (#3489) +- Fix bug in line length calculations for power operations (#3942) +- Add trailing commas to collection literals even if there's a comment after the last + entry (#3393) +- When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from + subscript expressions with more than 1 element (#3209) +- Add extra blank lines in stubs in a few cases (#3564, #3862) +- Accept raw strings as docstrings (#3947) +- Split long lines in case blocks (#4024) +- Stop removing spaces from walrus operators within subscripts (#3823) +- Fix incorrect formatting of certain async statements (#3609) +- Allow combining `# fmt: skip` with other comments (#3959) + ### Stable style -### Preview style - - +Several bug fixes were made in features that are moved to the stable style in this +release: - Fix comment handling when parenthesising conditional expressions (#4134) -- Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) - Remove empty lines before docstrings in async functions (#4132) - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) + +### Preview style + + + +- Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) diff --git a/src/black/comments.py b/src/black/comments.py index 52bb024a799..910e1b760f0 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Collection, Final, Iterator, List, Optional, Tuple, Union -from black.mode import Mode, Preview +from black.mode import Mode from black.nodes import ( CLOSING_BRACKETS, STANDALONE_COMMENT, @@ -390,22 +390,18 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: # noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview) # pylint:XXX; fmt:skip <-- list of comments (; separated, Preview) """ - semantic_comment_blocks = ( - [ - comment_line, - *[ - _COMMENT_PREFIX + comment.strip() - for comment in comment_line.split(_COMMENT_PREFIX)[1:] - ], - *[ - _COMMENT_PREFIX + comment.strip() - for comment in comment_line.strip(_COMMENT_PREFIX).split( - _COMMENT_LIST_SEPARATOR - ) - ], - ] - if Preview.single_line_format_skip_with_multiple_comments in mode - else [comment_line] - ) + semantic_comment_blocks = [ + comment_line, + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.split(_COMMENT_PREFIX)[1:] + ], + *[ + _COMMENT_PREFIX + comment.strip() + for comment in comment_line.strip(_COMMENT_PREFIX).split( + _COMMENT_LIST_SEPARATOR + ) + ], + ] return any(comment in FMT_SKIP for comment in semantic_comment_blocks) diff --git a/src/black/linegen.py b/src/black/linegen.py index dd296eb801d..a276805f2fe 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -115,10 +115,8 @@ def line(self, indent: int = 0) -> Iterator[Line]: self.current_line.depth += indent return # Line is empty, don't emit. Creating a new one unnecessary. - if ( - Preview.improved_async_statements_handling in self.mode - and len(self.current_line.leaves) == 1 - and is_async_stmt_or_funcdef(self.current_line.leaves[0]) + if len(self.current_line.leaves) == 1 and is_async_stmt_or_funcdef( + self.current_line.leaves[0] ): # Special case for async def/for/with statements. `visit_async_stmt` # adds an `ASYNC` leaf then visits the child def/for/with statement @@ -164,20 +162,19 @@ def visit_default(self, node: LN) -> Iterator[Line]: def visit_test(self, node: Node) -> Iterator[Line]: """Visit an `x if y else z` test""" - if Preview.parenthesize_conditional_expressions in self.mode: - already_parenthesized = ( - node.prev_sibling and node.prev_sibling.type == token.LPAR - ) + already_parenthesized = ( + node.prev_sibling and node.prev_sibling.type == token.LPAR + ) - if not already_parenthesized: - # Similar to logic in wrap_in_parentheses - lpar = Leaf(token.LPAR, "") - rpar = Leaf(token.RPAR, "") - prefix = node.prefix - node.prefix = "" - lpar.prefix = prefix - node.insert_child(0, lpar) - node.append_child(rpar) + if not already_parenthesized: + # Similar to logic in wrap_in_parentheses + lpar = Leaf(token.LPAR, "") + rpar = Leaf(token.RPAR, "") + prefix = node.prefix + node.prefix = "" + lpar.prefix = prefix + node.insert_child(0, lpar) + node.append_child(rpar) yield from self.visit_default(node) @@ -292,9 +289,7 @@ def visit_match_case(self, node: Node) -> Iterator[Line]: def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" - if ( - self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_suite(node, self.mode): + if is_stub_suite(node): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -308,11 +303,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: prev_type = child.type if node.parent and node.parent.type in STATEMENT: - if Preview.dummy_implementations in self.mode: - condition = is_parent_function_or_class(node) - else: - condition = self.mode.is_pyi - if condition and is_stub_body(node): + if is_parent_function_or_class(node) and is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) @@ -320,11 +311,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: yield from self.line(-1) else: - if ( - not (self.mode.is_pyi or Preview.dummy_implementations in self.mode) - or not node.parent - or not is_stub_suite(node.parent, self.mode) - ): + if not node.parent or not is_stub_suite(node.parent): yield from self.line() yield from self.visit_default(node) @@ -342,11 +329,7 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]: break internal_stmt = next(children) - if Preview.improved_async_statements_handling in self.mode: - yield from self.visit(internal_stmt) - else: - for child in internal_stmt.children: - yield from self.visit(child) + yield from self.visit(internal_stmt) def visit_decorators(self, node: Node) -> Iterator[Line]: """Visit decorators.""" @@ -420,10 +403,9 @@ def foo(a: int, b: float = 7): ... def foo(a: (int), b: (float) = 7): ... """ - if Preview.parenthesize_long_type_hints in self.mode: - assert len(node.children) == 3 - if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): - wrap_in_parentheses(node, node.children[2], visible=False) + assert len(node.children) == 3 + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + wrap_in_parentheses(node, node.children[2], visible=False) yield from self.visit_default(node) @@ -529,13 +511,7 @@ def __post_init__(self) -> None: self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) - # When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py - if Preview.parenthesize_long_type_hints in self.mode: - assignments = ASSIGNMENTS | {":"} - else: - assignments = ASSIGNMENTS - self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments) - + self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) @@ -576,9 +552,7 @@ def transform_line( # We need the line string when power operators are hugging to determine if we should # split the line. Default to line_str, if no power operator are present on the line. line_str_hugging_power_ops = ( - (_hugging_power_ops_line_to_string(line, features, mode) or line_str) - if Preview.fix_power_op_line_length in mode - else line_str + _hugging_power_ops_line_to_string(line, features, mode) or line_str ) ll = mode.line_length @@ -688,9 +662,6 @@ def should_split_funcdef_with_rhs(line: Line, mode: Mode) -> bool: """If a funcdef has a magic trailing comma in the return type, then we should first split the line with rhs to respect the comma. """ - if Preview.respect_magic_trailing_comma_in_return_type not in mode: - return False - return_type_leaves: List[Leaf] = [] in_return_type = False @@ -919,9 +890,6 @@ def _maybe_split_omitting_optional_parens( try: # The RHSResult Omitting Optional Parens. rhs_oop = _first_right_hand_split(line, omit=omit) - prefer_splitting_rhs_mode = ( - Preview.prefer_splitting_right_hand_side_of_assignments in line.mode - ) is_split_right_after_equal = ( len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL ) @@ -937,8 +905,7 @@ def _maybe_split_omitting_optional_parens( ) if ( not ( - prefer_splitting_rhs_mode - and is_split_right_after_equal + is_split_right_after_equal and rhs_head_contains_brackets and rhs_head_short_enough and rhs_head_explode_blocked_by_magic_trailing_comma @@ -1224,11 +1191,7 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features ) - if ( - Preview.add_trailing_comma_consistently in mode - and last_leaf.type == STANDALONE_COMMENT - and leaf_idx == last_non_comment_leaf - ): + if last_leaf.type == STANDALONE_COMMENT and leaf_idx == last_non_comment_leaf: current_line = _safe_add_trailing_comma( trailing_comma_safe, delimiter_priority, current_line ) @@ -1315,11 +1278,7 @@ def normalize_invisible_parens( # noqa: C901 # Fixes a bug where invisible parens are not properly wrapped around # case blocks. - if ( - isinstance(child, Node) - and child.type == syms.case_block - and Preview.long_case_block_line_splitting in mode - ): + if isinstance(child, Node) and child.type == syms.case_block: normalize_invisible_parens( child, parens_after={"case"}, mode=mode, features=features ) @@ -1374,7 +1333,6 @@ def normalize_invisible_parens( # noqa: C901 and child.next_sibling is not None and child.next_sibling.type == token.COLON and child.value == "case" - and Preview.long_case_block_line_splitting in mode ): # A special patch for "case case:" scenario, the second occurrence # of case will be not parsed as a Python keyword. @@ -1448,7 +1406,6 @@ def _maybe_wrap_cms_in_parens( """ if ( Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features - or Preview.wrap_multiple_context_managers_in_parens not in mode or len(node.children) <= 2 # If it's an atom, it's already wrapped in parens. or node.children[1].type == syms.atom diff --git a/src/black/lines.py b/src/black/lines.py index 9eb5785da57..29f87137614 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -202,9 +202,7 @@ def _is_triple_quoted_string(self) -> bool: value = self.leaves[0].value if value.startswith(('"""', "'''")): return True - if Preview.accept_raw_docstrings in self.mode and value.startswith( - ("r'''", 'r"""', "R'''", 'R"""') - ): + if value.startswith(("r'''", 'r"""', "R'''", 'R"""')): return True return False @@ -450,14 +448,8 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: if subscript_start.type == syms.subscriptlist: subscript_start = child_towards(subscript_start, leaf) - # When this is moved out of preview, add syms.namedexpr_test directly to - # TEST_DESCENDANTS in nodes.py - if Preview.walrus_subscript in self.mode: - test_decendants = TEST_DESCENDANTS | {syms.namedexpr_test} - else: - test_decendants = TEST_DESCENDANTS return subscript_start is not None and any( - n.type in test_decendants for n in subscript_start.pre_order() + n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() ) def enumerate_with_length( @@ -567,8 +559,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: lines (two on module-level). """ form_feed = ( - Preview.allow_form_feeds in self.mode - and current_line.depth == 0 + current_line.depth == 0 and bool(current_line.leaves) and "\f\n" in current_line.leaves[0].prefix ) @@ -582,8 +573,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: else before - previous_after ) if ( - Preview.module_docstring_newlines in current_line.mode - and self.previous_block + self.previous_block and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 and self.previous_block.original_line.is_docstring @@ -640,11 +630,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if previous_def is not None: assert self.previous_line is not None if self.mode.is_pyi: - if ( - Preview.blank_line_after_nested_stub_class in self.mode - and previous_def.is_class - and not previous_def.is_stub_class - ): + if previous_def.is_class and not previous_def.is_stub_class: before = 1 elif depth and not current_line.is_def and self.previous_line.is_def: # Empty lines between attributes and methods should be preserved. @@ -695,18 +681,12 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: and self.previous_line.is_class and current_line.is_docstring ): - if Preview.no_blank_line_before_class_docstring in current_line.mode: - return 0, 1 - return before, 1 + return 0, 1 # In preview mode, always allow blank lines, except right before a function # docstring - is_empty_first_line_ok = ( - Preview.allow_empty_first_line_in_block in current_line.mode - and ( - not current_line.is_docstring - or (self.previous_line and not self.previous_line.is_def) - ) + is_empty_first_line_ok = not current_line.is_docstring or ( + self.previous_line and not self.previous_line.is_def ) if ( @@ -736,7 +716,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 if self.previous_line.depth < current_line.depth and ( self.previous_line.is_class or self.previous_line.is_def ): - if self.mode.is_pyi or not Preview.allow_empty_first_line_in_block: + if self.mode.is_pyi: return 0, 0 else: return 1 if user_had_newline else 0, 0 @@ -776,10 +756,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 # Don't inspect the previous line if it's part of the body of the previous # statement in the same level, we always want a blank line if there's # something with a body preceding. - elif ( - Preview.blank_line_between_nested_and_def_stub_file in current_line.mode - and self.previous_line.depth > current_line.depth - ): + elif self.previous_line.depth > current_line.depth: newlines = 1 elif ( current_line.is_def or current_line.is_decorator @@ -800,11 +777,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 newlines = 1 if current_line.depth else 2 # If a user has left no space after a dummy implementation, don't insert # new lines. This is useful for instance for @overload or Protocols. - if ( - Preview.dummy_implementations in self.mode - and self.previous_line.is_stub_def - and not user_had_newline - ): + if self.previous_line.is_stub_def and not user_had_newline: newlines = 0 if comment_to_add_newlines is not None: previous_block = comment_to_add_newlines.previous_block @@ -859,11 +832,9 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) - width = str_width if Preview.respect_east_asian_width in mode else len - if Preview.multiline_string_handling not in mode: return ( - width(line_str) <= mode.line_length + str_width(line_str) <= mode.line_length and "\n" not in line_str # multiline strings and not line.contains_standalone_comments() ) @@ -872,10 +843,10 @@ def is_line_short_enough( # noqa: C901 return False if "\n" not in line_str: # No multiline strings (MLS) present - return width(line_str) <= mode.line_length + return str_width(line_str) <= mode.line_length first, *_, last = line_str.split("\n") - if width(first) > mode.line_length or width(last) > mode.line_length: + if str_width(first) > mode.line_length or str_width(last) > mode.line_length: return False # Traverse the AST to examine the context of the multiline string (MLS), @@ -1015,11 +986,7 @@ def can_omit_invisible_parens( return False if delimiter_count == 1: - if ( - Preview.wrap_multiple_context_managers_in_parens in line.mode - and max_priority == COMMA_PRIORITY - and rhs.head.is_with_or_async_with_stmt - ): + if max_priority == COMMA_PRIORITY and rhs.head.is_with_or_async_with_stmt: # For two context manager with statements, the optional parentheses read # better. In this case, `rhs.body` is the context managers part of # the with statement. `rhs.head` is the `with (` part on the previous diff --git a/src/black/mode.py b/src/black/mode.py index 466b78228fc..1b97f3508ee 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -168,35 +168,14 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b class Preview(Enum): """Individual preview style features.""" - add_trailing_comma_consistently = auto() - blank_line_after_nested_stub_class = auto() - blank_line_between_nested_and_def_stub_file = auto() hex_codes_in_unicode_sequences = auto() - improved_async_statements_handling = auto() - multiline_string_handling = auto() - no_blank_line_before_class_docstring = auto() - prefer_splitting_right_hand_side_of_assignments = auto() # NOTE: string_processing requires wrap_long_dict_values_in_parens # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() - parenthesize_conditional_expressions = auto() - parenthesize_long_type_hints = auto() - respect_magic_trailing_comma_in_return_type = auto() - skip_magic_trailing_comma_in_subscript = auto() - wrap_long_dict_values_in_parens = auto() - wrap_multiple_context_managers_in_parens = auto() - dummy_implementations = auto() - walrus_subscript = auto() - module_docstring_newlines = auto() - accept_raw_docstrings = auto() - fix_power_op_line_length = auto() hug_parens_with_braces_and_square_brackets = auto() - allow_empty_first_line_in_block = auto() - single_line_format_skip_with_multiple_comments = auto() - long_case_block_line_splitting = auto() - allow_form_feeds = auto() unify_docstring_detection = auto() - respect_east_asian_width = auto() + wrap_long_dict_values_in_parens = auto() + multiline_string_handling = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index 7ee2df2e061..a8869cba234 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -104,6 +104,7 @@ syms.trailer, syms.term, syms.power, + syms.namedexpr_test, } TYPED_NAMES: Final = {syms.tname, syms.tname_star} ASSIGNMENTS: Final = { @@ -121,6 +122,7 @@ ">>=", "**=", "//=", + ":", } IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist} @@ -346,9 +348,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no return NO - elif Preview.walrus_subscript in mode and ( - t == token.COLONEQUAL or prev.type == token.COLONEQUAL - ): + elif t == token.COLONEQUAL or prev.type == token.COLONEQUAL: return SPACE elif not complex_subscript: @@ -753,13 +753,9 @@ def is_function_or_class(node: Node) -> bool: return node.type in {syms.funcdef, syms.classdef, syms.async_funcdef} -def is_stub_suite(node: Node, mode: Mode) -> bool: +def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" - if ( - node.parent is not None - and Preview.dummy_implementations in mode - and not is_parent_function_or_class(node) - ): + if node.parent is not None and not is_parent_function_or_class(node): return False # If there is a comment, we want to keep it. diff --git a/tests/data/cases/preview_allow_empty_first_line.py b/tests/data/cases/allow_empty_first_line.py similarity index 98% rename from tests/data/cases/preview_allow_empty_first_line.py rename to tests/data/cases/allow_empty_first_line.py index 4269987305d..32a170a97d0 100644 --- a/tests/data/cases/preview_allow_empty_first_line.py +++ b/tests/data/cases/allow_empty_first_line.py @@ -1,4 +1,3 @@ -# flags: --preview def foo(): """ Docstring diff --git a/tests/data/cases/preview_async_stmts.py b/tests/data/cases/async_stmts.py similarity index 93% rename from tests/data/cases/preview_async_stmts.py rename to tests/data/cases/async_stmts.py index 0a7671be5a6..fe9594b2164 100644 --- a/tests/data/cases/preview_async_stmts.py +++ b/tests/data/cases/async_stmts.py @@ -1,4 +1,3 @@ -# flags: --preview async def func() -> (int): return 0 diff --git a/tests/data/cases/comments5.py b/tests/data/cases/comments5.py index bda40619f62..4270d3a09a2 100644 --- a/tests/data/cases/comments5.py +++ b/tests/data/cases/comments5.py @@ -45,8 +45,7 @@ def wat(): @deco2(with_args=True) # leading 3 @deco3 -def decorated1(): - ... +def decorated1(): ... # leading 1 @@ -54,8 +53,7 @@ def decorated1(): # leading 2 @deco2(with_args=True) # leading function comment -def decorated1(): - ... +def decorated1(): ... # Note: this is fixed in @@ -65,8 +63,7 @@ def decorated1(): # This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... +def g(): ... if __name__ == "__main__": diff --git a/tests/data/cases/conditional_expression.py b/tests/data/cases/conditional_expression.py index 76251bd9318..f65d6fb00e7 100644 --- a/tests/data/cases/conditional_expression.py +++ b/tests/data/cases/conditional_expression.py @@ -1,4 +1,3 @@ -# flags: --preview long_kwargs_single_line = my_function( foo="test, this is a sample value", bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, @@ -197,7 +196,9 @@ def foo(wait: bool = True): time.sleep(1) if wait else None -a = "".join(( - "", # comment - "" if True else "", -)) +a = "".join( + ( + "", # comment + "" if True else "", + ) +) diff --git a/tests/data/cases/preview_context_managers_38.py b/tests/data/cases/context_managers_38.py similarity index 96% rename from tests/data/cases/preview_context_managers_38.py rename to tests/data/cases/context_managers_38.py index 719d94fdcc5..54fb97c708b 100644 --- a/tests/data/cases/preview_context_managers_38.py +++ b/tests/data/cases/context_managers_38.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.8 +# flags: --minimum-version=3.8 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/cases/preview_context_managers_39.py b/tests/data/cases/context_managers_39.py similarity index 98% rename from tests/data/cases/preview_context_managers_39.py rename to tests/data/cases/context_managers_39.py index 589e00ad187..60fd1a56409 100644 --- a/tests/data/cases/preview_context_managers_39.py +++ b/tests/data/cases/context_managers_39.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.9 +# flags: --minimum-version=3.9 with \ make_context_manager1() as cm1, \ make_context_manager2() as cm2, \ diff --git a/tests/data/cases/preview_context_managers_autodetect_310.py b/tests/data/cases/context_managers_autodetect_310.py similarity index 93% rename from tests/data/cases/preview_context_managers_autodetect_310.py rename to tests/data/cases/context_managers_autodetect_310.py index a9e31076f03..80f211032e5 100644 --- a/tests/data/cases/preview_context_managers_autodetect_310.py +++ b/tests/data/cases/context_managers_autodetect_310.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 # This file uses pattern matching introduced in Python 3.10. diff --git a/tests/data/cases/preview_context_managers_autodetect_311.py b/tests/data/cases/context_managers_autodetect_311.py similarity index 92% rename from tests/data/cases/preview_context_managers_autodetect_311.py rename to tests/data/cases/context_managers_autodetect_311.py index af1e83fe74c..020c4cea967 100644 --- a/tests/data/cases/preview_context_managers_autodetect_311.py +++ b/tests/data/cases/context_managers_autodetect_311.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.11 +# flags: --minimum-version=3.11 # This file uses except* clause in Python 3.11. diff --git a/tests/data/cases/preview_context_managers_autodetect_38.py b/tests/data/cases/context_managers_autodetect_38.py similarity index 98% rename from tests/data/cases/preview_context_managers_autodetect_38.py rename to tests/data/cases/context_managers_autodetect_38.py index 25217a40604..79e438b995e 100644 --- a/tests/data/cases/preview_context_managers_autodetect_38.py +++ b/tests/data/cases/context_managers_autodetect_38.py @@ -1,4 +1,3 @@ -# flags: --preview # This file doesn't use any Python 3.9+ only grammars. diff --git a/tests/data/cases/preview_context_managers_autodetect_39.py b/tests/data/cases/context_managers_autodetect_39.py similarity index 93% rename from tests/data/cases/preview_context_managers_autodetect_39.py rename to tests/data/cases/context_managers_autodetect_39.py index 3f72e48db9d..98e674b2f9d 100644 --- a/tests/data/cases/preview_context_managers_autodetect_39.py +++ b/tests/data/cases/context_managers_autodetect_39.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.9 +# flags: --minimum-version=3.9 # This file uses parenthesized context managers introduced in Python 3.9. diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/dummy_implementations.py similarity index 99% rename from tests/data/cases/preview_dummy_implementations.py rename to tests/data/cases/dummy_implementations.py index 3cd392c9587..0a52c081bcc 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/dummy_implementations.py @@ -1,4 +1,3 @@ -# flags: --preview from typing import NoReturn, Protocol, Union, overload class Empty: diff --git a/tests/data/cases/empty_lines.py b/tests/data/cases/empty_lines.py index 4fd47b93dca..4c03e432383 100644 --- a/tests/data/cases/empty_lines.py +++ b/tests/data/cases/empty_lines.py @@ -119,6 +119,7 @@ def f(): if not prev: prevp = preceding_leaf(p) if not prevp or prevp.type in OPENING_BRACKETS: + return NO if prevp.type == token.EQUAL: diff --git a/tests/data/cases/fmtonoff.py b/tests/data/cases/fmtonoff.py index d1f15cd5c8b..8af94563af8 100644 --- a/tests/data/cases/fmtonoff.py +++ b/tests/data/cases/fmtonoff.py @@ -243,12 +243,8 @@ def spaces_types( g: int = 1 if False else 2, h: str = "", i: str = r"", -): - ... - - -def spaces2(result=_core.Value(None)): - ... +): ... +def spaces2(result=_core.Value(None)): ... something = { diff --git a/tests/data/cases/fmtonoff5.py b/tests/data/cases/fmtonoff5.py index 181151b6bd6..4c134a9eea3 100644 --- a/tests/data/cases/fmtonoff5.py +++ b/tests/data/cases/fmtonoff5.py @@ -161,8 +161,7 @@ def this_wont_be_formatted ( self ) -> str: ... class Factory(t.Protocol): - def this_will_be_formatted(self, **kwargs) -> Named: - ... + def this_will_be_formatted(self, **kwargs) -> Named: ... # fmt: on diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/form_feeds.py similarity index 99% rename from tests/data/cases/preview_form_feeds.py rename to tests/data/cases/form_feeds.py index dc3bd6cfe2e..48ffc98106b 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/form_feeds.py @@ -1,4 +1,3 @@ -# flags: --preview # Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). diff --git a/tests/data/cases/function.py b/tests/data/cases/function.py index 2d642c8731b..4e3f91fd8b1 100644 --- a/tests/data/cases/function.py +++ b/tests/data/cases/function.py @@ -158,10 +158,7 @@ def spaces_types( g: int = 1 if False else 2, h: str = "", i: str = r"", -): - ... - - +): ... def spaces2(result=_core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) diff --git a/tests/data/cases/remove_newline_after_match.py b/tests/data/cases/keep_newline_after_match.py similarity index 98% rename from tests/data/cases/remove_newline_after_match.py rename to tests/data/cases/keep_newline_after_match.py index fe6592b664d..dbeccce6264 100644 --- a/tests/data/cases/remove_newline_after_match.py +++ b/tests/data/cases/keep_newline_after_match.py @@ -21,15 +21,21 @@ def http_status(status): # output def http_status(status): + match status: + case 400: + return "Bad request" case 401: + return "Unauthorized" case 403: + return "Forbidden" case 404: + return "Not found" \ No newline at end of file diff --git a/tests/data/cases/long_strings_flag_disabled.py b/tests/data/cases/long_strings_flag_disabled.py index db3954e3abd..d81c331cab2 100644 --- a/tests/data/cases/long_strings_flag_disabled.py +++ b/tests/data/cases/long_strings_flag_disabled.py @@ -43,8 +43,10 @@ % ( "formatted", "string", - ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." - % ("soooo", 2), + ): ( + "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2) + ), } func_with_keywords( @@ -254,10 +256,12 @@ + CONCATENATED + "using the '+' operator." ) -annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -annotated_variable: Literal[ - "fakse_literal" -] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Final = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) +annotated_variable: Literal["fakse_literal"] = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" diff --git a/tests/data/cases/module_docstring_1.py b/tests/data/cases/module_docstring_1.py index d5897b4db60..5751154f7f0 100644 --- a/tests/data/cases/module_docstring_1.py +++ b/tests/data/cases/module_docstring_1.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index 1cc9aea9aea..ac486096c02 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -1,7 +1,7 @@ # flags: --preview """I am a very helpful module docstring. -With trailing spaces: +With trailing spaces (only removed with unify_docstring_detection on): Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, @@ -39,7 +39,7 @@ # output """I am a very helpful module docstring. -With trailing spaces: +With trailing spaces (only removed with unify_docstring_detection on): Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, diff --git a/tests/data/cases/module_docstring_3.py b/tests/data/cases/module_docstring_3.py index 0631e136a3d..3d0058dd554 100644 --- a/tests/data/cases/module_docstring_3.py +++ b/tests/data/cases/module_docstring_3.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" a = 1 diff --git a/tests/data/cases/module_docstring_4.py b/tests/data/cases/module_docstring_4.py index 515174dcc04..b1720078f71 100644 --- a/tests/data/cases/module_docstring_4.py +++ b/tests/data/cases/module_docstring_4.py @@ -1,4 +1,3 @@ -# flags: --preview """Single line module-level docstring should be followed by single newline.""" a = 1 diff --git a/tests/data/cases/module_docstring_followed_by_class.py b/tests/data/cases/module_docstring_followed_by_class.py index 6fdbfc8c240..c291e61b960 100644 --- a/tests/data/cases/module_docstring_followed_by_class.py +++ b/tests/data/cases/module_docstring_followed_by_class.py @@ -1,4 +1,3 @@ -# flags: --preview """Two blank lines between module docstring and a class.""" class MyClass: pass diff --git a/tests/data/cases/module_docstring_followed_by_function.py b/tests/data/cases/module_docstring_followed_by_function.py index 5913a59e1fe..fd29b98da8e 100644 --- a/tests/data/cases/module_docstring_followed_by_function.py +++ b/tests/data/cases/module_docstring_followed_by_function.py @@ -1,4 +1,3 @@ -# flags: --preview """Two blank lines between module docstring and a function def.""" def function(): pass diff --git a/tests/data/cases/nested_stub.py b/tests/data/cases/nested_stub.py index ef13c588ce6..40ca11e9330 100644 --- a/tests/data/cases/nested_stub.py +++ b/tests/data/cases/nested_stub.py @@ -1,4 +1,4 @@ -# flags: --pyi --preview +# flags: --pyi import sys class Outer: diff --git a/tests/data/cases/preview_no_blank_line_before_docstring.py b/tests/data/cases/no_blank_line_before_docstring.py similarity index 98% rename from tests/data/cases/preview_no_blank_line_before_docstring.py rename to tests/data/cases/no_blank_line_before_docstring.py index faeaa1e46e4..ced125fef78 100644 --- a/tests/data/cases/preview_no_blank_line_before_docstring.py +++ b/tests/data/cases/no_blank_line_before_docstring.py @@ -1,4 +1,3 @@ -# flags: --preview def line_before_docstring(): """Please move me up""" @@ -63,4 +62,5 @@ class MultilineDocstringsAsWell: class SingleQuotedDocstring: + "I'm a docstring but I don't even get triple quotes." diff --git a/tests/data/cases/preview_pattern_matching_long.py b/tests/data/cases/pattern_matching_long.py similarity index 94% rename from tests/data/cases/preview_pattern_matching_long.py rename to tests/data/cases/pattern_matching_long.py index df849fdc4f2..9a944c9d0c9 100644 --- a/tests/data/cases/preview_pattern_matching_long.py +++ b/tests/data/cases/pattern_matching_long.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 match x: case "abcd" | "abcd" | "abcd" : pass diff --git a/tests/data/cases/preview_pattern_matching_trailing_comma.py b/tests/data/cases/pattern_matching_trailing_comma.py similarity index 92% rename from tests/data/cases/preview_pattern_matching_trailing_comma.py rename to tests/data/cases/pattern_matching_trailing_comma.py index e6c0d88bb80..5660b0f6a14 100644 --- a/tests/data/cases/preview_pattern_matching_trailing_comma.py +++ b/tests/data/cases/pattern_matching_trailing_comma.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 match maybe, multiple: case perhaps, 5: pass diff --git a/tests/data/cases/pep604_union_types_line_breaks.py b/tests/data/cases/pep604_union_types_line_breaks.py index fee2b840494..745bc9e8b02 100644 --- a/tests/data/cases/pep604_union_types_line_breaks.py +++ b/tests/data/cases/pep604_union_types_line_breaks.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 # This has always worked z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong diff --git a/tests/data/cases/pep_572_py310.py b/tests/data/cases/pep_572_py310.py index 9f999deeb89..ba488d4741c 100644 --- a/tests/data/cases/pep_572_py310.py +++ b/tests/data/cases/pep_572_py310.py @@ -1,8 +1,8 @@ # flags: --minimum-version=3.10 # Unparenthesized walruses are now allowed in indices since Python 3.10. -x[a:=0] -x[a:=0, b:=1] -x[5, b:=0] +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] # Walruses are allowed inside generator expressions on function calls since 3.10. if any(match := pattern_error.match(s) for s in buffer): diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 24f1ac29168..f0026ceb032 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -96,18 +96,16 @@ async def await_the_walrus(): foo(x=(y := f(x))) -def foo(answer=(p := 42)): - ... +def foo(answer=(p := 42)): ... -def foo2(answer: (p := 42) = 5): - ... +def foo2(answer: (p := 42) = 5): ... lambda: (x := 1) a[(x := 12)] -a[:(x := 13)] +a[: (x := 13)] # we don't touch expressions in f-strings but if we do one day, don't break 'em f"{(x:=10)}" diff --git a/tests/data/cases/preview_pep_572.py b/tests/data/cases/pep_572_slices.py similarity index 75% rename from tests/data/cases/preview_pep_572.py rename to tests/data/cases/pep_572_slices.py index 75ad0cc4176..aa772b1f1f5 100644 --- a/tests/data/cases/preview_pep_572.py +++ b/tests/data/cases/pep_572_slices.py @@ -1,4 +1,3 @@ -# flags: --preview x[(a:=0):] x[:(a:=0)] diff --git a/tests/data/cases/preview_percent_precedence.py b/tests/data/cases/percent_precedence.py similarity index 91% rename from tests/data/cases/preview_percent_precedence.py rename to tests/data/cases/percent_precedence.py index aeaf450ff5e..7822e42c69d 100644 --- a/tests/data/cases/preview_percent_precedence.py +++ b/tests/data/cases/percent_precedence.py @@ -1,4 +1,3 @@ -# flags: --preview ("" % a) ** 2 ("" % a)[0] ("" % a)() @@ -31,9 +30,9 @@ 2 // ("" % a) 2 % ("" % a) +("" % a) -b + "" % a +b + ("" % a) -("" % a) -b - "" % a +b - ("" % a) b + -("" % a) ~("" % a) 2 ** ("" % a) diff --git a/tests/data/cases/preview_power_op_spacing.py b/tests/data/cases/power_op_spacing_long.py similarity index 99% rename from tests/data/cases/preview_power_op_spacing.py rename to tests/data/cases/power_op_spacing_long.py index 650c6fecb20..30e6eb788b3 100644 --- a/tests/data/cases/preview_power_op_spacing.py +++ b/tests/data/cases/power_op_spacing_long.py @@ -1,4 +1,3 @@ -# flags: --preview a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 b = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1 c = 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 diff --git a/tests/data/cases/preview_prefer_rhs_split.py b/tests/data/cases/prefer_rhs_split.py similarity index 99% rename from tests/data/cases/preview_prefer_rhs_split.py rename to tests/data/cases/prefer_rhs_split.py index 28d89c368c0..f3d9fd67251 100644 --- a/tests/data/cases/preview_prefer_rhs_split.py +++ b/tests/data/cases/prefer_rhs_split.py @@ -1,4 +1,3 @@ -# flags: --preview first_item, second_item = ( some_looooooooong_module.some_looooooooooooooong_function_name( first_argument, second_argument, third_argument diff --git a/tests/data/cases/py310_pep572.py b/tests/data/cases/py310_pep572.py index 172be3898d6..73fbe44d42c 100644 --- a/tests/data/cases/py310_pep572.py +++ b/tests/data/cases/py310_pep572.py @@ -1,4 +1,4 @@ -# flags: --preview --minimum-version=3.10 +# flags: --minimum-version=3.10 x[a:=0] x[a := 0] x[a := 0, b := 1] diff --git a/tests/data/cases/python39.py b/tests/data/cases/python39.py index 1b9536c1529..85eddc38e00 100644 --- a/tests/data/cases/python39.py +++ b/tests/data/cases/python39.py @@ -15,19 +15,16 @@ def f(): # output @relaxed_decorator[0] -def f(): - ... +def f(): ... @relaxed_decorator[ extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length ] -def f(): - ... +def f(): ... @extremely_long_variable_name_that_doesnt_fit := complex.expression( with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" ) -def f(): - ... \ No newline at end of file +def f(): ... \ No newline at end of file diff --git a/tests/data/cases/raw_docstring.py b/tests/data/cases/raw_docstring.py index 751fd3201df..7f88bb2de86 100644 --- a/tests/data/cases/raw_docstring.py +++ b/tests/data/cases/raw_docstring.py @@ -1,4 +1,4 @@ -# flags: --preview --skip-string-normalization +# flags: --skip-string-normalization class C: r"""Raw""" diff --git a/tests/data/cases/preview_docstring_no_string_normalization.py b/tests/data/cases/raw_docstring_no_string_normalization.py similarity index 88% rename from tests/data/cases/preview_docstring_no_string_normalization.py rename to tests/data/cases/raw_docstring_no_string_normalization.py index 712c7364f51..a201c1e8fae 100644 --- a/tests/data/cases/preview_docstring_no_string_normalization.py +++ b/tests/data/cases/raw_docstring_no_string_normalization.py @@ -1,4 +1,4 @@ -# flags: --preview --skip-string-normalization +# flags: --skip-string-normalization def do_not_touch_this_prefix(): R"""There was a bug where docstring prefixes would be normalized even with -S.""" diff --git a/tests/data/cases/remove_newline_after_code_block_open.py b/tests/data/cases/remove_newline_after_code_block_open.py index ef2e5c2f6f5..6622e8afb7d 100644 --- a/tests/data/cases/remove_newline_after_code_block_open.py +++ b/tests/data/cases/remove_newline_after_code_block_open.py @@ -3,14 +3,14 @@ def foo1(): - print("The newline above me should be deleted!") + print("The newline above me should be kept!") def foo2(): - print("All the newlines above me should be deleted!") + print("All the newlines above me should be kept!") def foo3(): @@ -30,31 +30,31 @@ def foo4(): class Foo: def bar(self): - print("The newline above me should be deleted!") + print("The newline above me should be kept!") for i in range(5): - print(f"{i}) The line above me should be removed!") + print(f"{i}) The line above me should be kept!") for i in range(5): - print(f"{i}) The lines above me should be removed!") + print(f"{i}) The lines above me should be kept!") for i in range(5): for j in range(7): - print(f"{i}) The lines above me should be removed!") + print(f"{i}) The lines above me should be kept!") if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") + print("The new line above me will be kept!") if random.randint(0, 3) == 0: @@ -62,43 +62,45 @@ def bar(self): - print("The new lines above me is about to be removed!") + print("The new lines above me will be kept!") if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") + + print("Two lines above me will be kept!") while True: - print("The newline above me should be deleted!") + print("The newline above me should be kept!") while True: - print("The newlines above me should be deleted!") + print("The newlines above me should be kept!") while True: while False: - print("The newlines above me should be deleted!") + print("The newlines above me should be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new line above me is about to be removed!") + file.write("The new line above me will be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new lines above me is about to be removed!") + file.write("The new lines above me will be kept!") with open("/path/to/file.txt", mode="r") as read_file: @@ -113,20 +115,24 @@ def bar(self): def foo1(): - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") def foo2(): - print("All the newlines above me should be deleted!") + + print("All the newlines above me should be kept!") def foo3(): + print("No newline above me!") print("There is a newline above me, and that's OK!") def foo4(): + # There is a comment here print("The newline above me should not be deleted!") @@ -134,56 +140,73 @@ def foo4(): class Foo: def bar(self): - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") for i in range(5): - print(f"{i}) The line above me should be removed!") + + print(f"{i}) The line above me should be kept!") for i in range(5): - print(f"{i}) The lines above me should be removed!") + + print(f"{i}) The lines above me should be kept!") for i in range(5): + for j in range(7): - print(f"{i}) The lines above me should be removed!") + + print(f"{i}) The lines above me should be kept!") if random.randint(0, 3) == 0: - print("The new line above me is about to be removed!") + + print("The new line above me will be kept!") if random.randint(0, 3) == 0: - print("The new lines above me is about to be removed!") + + print("The new lines above me will be kept!") if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") + + print("Two lines above me will be kept!") while True: - print("The newline above me should be deleted!") + + print("The newline above me should be kept!") while True: - print("The newlines above me should be deleted!") + + print("The newlines above me should be kept!") while True: + while False: - print("The newlines above me should be deleted!") + + print("The newlines above me should be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new line above me is about to be removed!") + + file.write("The new line above me will be kept!") with open("/path/to/file.txt", mode="w") as file: - file.write("The new lines above me is about to be removed!") + + file.write("The new lines above me will be kept!") with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) diff --git a/tests/data/cases/return_annotation_brackets.py b/tests/data/cases/return_annotation_brackets.py index 8509ecdb92c..ed05bed61f4 100644 --- a/tests/data/cases/return_annotation_brackets.py +++ b/tests/data/cases/return_annotation_brackets.py @@ -88,7 +88,6 @@ def foo() -> tuple[int, int, int,]: return 2 # Magic trailing comma example, with params -# this is broken - the trailing comma is transferred to the param list. Fixed in preview def foo(a,b) -> tuple[int, int, int,]: return 2 @@ -194,30 +193,27 @@ def foo() -> tuple[int, int, int]: return 2 -def foo() -> ( - tuple[ - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ] -): +def foo() -> tuple[ + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, +]: return 2 # Magic trailing comma example -def foo() -> ( - tuple[ - int, - int, - int, - ] -): +def foo() -> tuple[ + int, + int, + int, +]: return 2 # Magic trailing comma example, with params -# this is broken - the trailing comma is transferred to the param list. Fixed in preview -def foo( - a, b -) -> tuple[int, int, int,]: +def foo(a, b) -> tuple[ + int, + int, + int, +]: return 2 diff --git a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py b/tests/data/cases/single_line_format_skip_with_multiple_comments.py similarity index 97% rename from tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py rename to tests/data/cases/single_line_format_skip_with_multiple_comments.py index efde662baa8..7212740fc42 100644 --- a/tests/data/cases/preview_single_line_format_skip_with_multiple_comments.py +++ b/tests/data/cases/single_line_format_skip_with_multiple_comments.py @@ -1,4 +1,3 @@ -# flags: --preview foo = 123 # fmt: skip # noqa: E501 # pylint bar = ( 123 , diff --git a/tests/data/cases/preview_trailing_comma.py b/tests/data/cases/trailing_comma.py similarity index 97% rename from tests/data/cases/preview_trailing_comma.py rename to tests/data/cases/trailing_comma.py index bba7e7ad16d..5b09c664606 100644 --- a/tests/data/cases/preview_trailing_comma.py +++ b/tests/data/cases/trailing_comma.py @@ -1,4 +1,3 @@ -# flags: --preview e = { "a": fun(msg, "ts"), "longggggggggggggggid": ..., diff --git a/tests/data/cases/walrus_in_dict.py b/tests/data/cases/walrus_in_dict.py index c33eecd84a6..c91ad9e8611 100644 --- a/tests/data/cases/walrus_in_dict.py +++ b/tests/data/cases/walrus_in_dict.py @@ -1,7 +1,9 @@ # flags: --preview +# This is testing an issue that is specific to the preview style { "is_update": (up := commit.hash in update_hashes) } # output +# This is testing an issue that is specific to the preview style {"is_update": (up := commit.hash in update_hashes)} From a5196e6f1f450e4c8da0e514e01873a0cc1e1a3c Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:31:49 -0600 Subject: [PATCH 688/700] fix: Don't normalize whitespace before fmt:skip comments (#4146) Signed-off-by: RedGuy12 --- CHANGES.md | 1 + src/black/comments.py | 14 +++++++++++--- src/black/mode.py | 1 + tests/data/cases/fmtskip9.py | 9 +++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/data/cases/fmtskip9.py diff --git a/CHANGES.md b/CHANGES.md index 0e2974d706e..2fe14cd5246 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -56,6 +56,7 @@ release: - Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) +- Stop normalizing spaces before `# fmt: skip` comments (#4146) ### Configuration diff --git a/src/black/comments.py b/src/black/comments.py index 910e1b760f0..ea54e2468c9 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Collection, Final, Iterator, List, Optional, Tuple, Union -from black.mode import Mode +from black.mode import Mode, Preview from black.nodes import ( CLOSING_BRACKETS, STANDALONE_COMMENT, @@ -46,6 +46,7 @@ class ProtoComment: newlines: int # how many newlines before the comment consumed: int # how many characters of the original leaf's prefix did we consume form_feed: bool # is there a form feed before the comment + leading_whitespace: str # leading whitespace before the comment, if any def generate_comments(leaf: LN) -> Iterator[Leaf]: @@ -88,7 +89,9 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: form_feed = False for index, full_line in enumerate(re.split("\r?\n", prefix)): consumed += len(full_line) + 1 # adding the length of the split '\n' - line = full_line.lstrip() + match = re.match(r"^(\s*)(\S.*|)$", full_line) + assert match + whitespace, line = match.groups() if not line: nlines += 1 if "\f" in full_line: @@ -113,6 +116,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: newlines=nlines, consumed=consumed, form_feed=form_feed, + leading_whitespace=whitespace, ) ) form_feed = False @@ -230,7 +234,11 @@ def convert_one_fmt_off_pair( standalone_comment_prefix += fmt_off_prefix hidden_value = comment.value + "\n" + hidden_value if _contains_fmt_skip_comment(comment.value, mode): - hidden_value += " " + comment.value + hidden_value += ( + comment.leading_whitespace + if Preview.no_normalize_fmt_skip_whitespace in mode + else " " + ) + comment.value if hidden_value.endswith("\n"): # That happens when one of the `ignored_nodes` ended with a NEWLINE # leaf (possibly followed by a DEDENT). diff --git a/src/black/mode.py b/src/black/mode.py index 1b97f3508ee..a1519f17bcc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -174,6 +174,7 @@ class Preview(Enum): string_processing = auto() hug_parens_with_braces_and_square_brackets = auto() unify_docstring_detection = auto() + no_normalize_fmt_skip_whitespace = auto() wrap_long_dict_values_in_parens = auto() multiline_string_handling = auto() diff --git a/tests/data/cases/fmtskip9.py b/tests/data/cases/fmtskip9.py new file mode 100644 index 00000000000..30085bdd973 --- /dev/null +++ b/tests/data/cases/fmtskip9.py @@ -0,0 +1,9 @@ +# flags: --preview +print () # fmt: skip +print () # fmt:skip + + +# output + +print () # fmt: skip +print () # fmt:skip From f7d552d9b7bd33f43edd0867757c27b1aa36c651 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:11:26 -0800 Subject: [PATCH 689/700] Remove reference (#4169) This is out-of-date and just a chore. I don't think this is useful to contributors and Black doesn't even have a public API. --- docs/contributing/index.md | 5 - .../reference/reference_classes.rst | 234 ------------------ .../reference/reference_exceptions.rst | 18 -- .../reference/reference_functions.rst | 172 ------------- .../reference/reference_summary.rst | 19 -- 5 files changed, 448 deletions(-) delete mode 100644 docs/contributing/reference/reference_classes.rst delete mode 100644 docs/contributing/reference/reference_exceptions.rst delete mode 100644 docs/contributing/reference/reference_functions.rst delete mode 100644 docs/contributing/reference/reference_summary.rst diff --git a/docs/contributing/index.md b/docs/contributing/index.md index 3314c8eaa39..f15afa318d4 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -9,7 +9,6 @@ the_basics gauging_changes issue_triage release_process -reference/reference_summary ``` Welcome! Happy to see you willing to make the project better. Have you read the entire @@ -42,9 +41,5 @@ This section covers the following topics: - {doc}`the_basics` - {doc}`gauging_changes` - {doc}`release_process` -- {doc}`reference/reference_summary` For an overview on contributing to the _Black_, please checkout {doc}`the_basics`. - -If you need a reference of the functions, classes, etc. available to you while -developing _Black_, there's the {doc}`reference/reference_summary` docs. diff --git a/docs/contributing/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst deleted file mode 100644 index dc615579e30..00000000000 --- a/docs/contributing/reference/reference_classes.rst +++ /dev/null @@ -1,234 +0,0 @@ -*Black* classes -=============== - -*Contents are subject to change.* - -Black Classes -~~~~~~~~~~~~~~ - -.. currentmodule:: black - -:class:`BracketTracker` ------------------------ - -.. autoclass:: black.brackets.BracketTracker - :members: - -:class:`Line` -------------- - -.. autoclass:: black.lines.Line - :members: - :special-members: __str__, __bool__ - -:class:`RHSResult` -------------------------- - -.. autoclass:: black.lines.RHSResult - :members: - -:class:`LinesBlock` -------------------------- - -.. autoclass:: black.lines.LinesBlock - :members: - -:class:`EmptyLineTracker` -------------------------- - -.. autoclass:: black.lines.EmptyLineTracker - :members: - -:class:`LineGenerator` ----------------------- - -.. autoclass:: black.linegen.LineGenerator - :show-inheritance: - :members: - -:class:`ProtoComment` ---------------------- - -.. autoclass:: black.comments.ProtoComment - :members: - -:class:`Mode` ---------------------- - -.. autoclass:: black.mode.Mode - :members: - -:class:`Report` ---------------- - -.. autoclass:: black.report.Report - :members: - :special-members: __str__ - -:class:`Ok` ---------------- - -.. autoclass:: black.rusty.Ok - :show-inheritance: - :members: - -:class:`Err` ---------------- - -.. autoclass:: black.rusty.Err - :show-inheritance: - :members: - -:class:`Visitor` ----------------- - -.. autoclass:: black.nodes.Visitor - :show-inheritance: - :members: - -:class:`StringTransformer` ----------------------------- - -.. autoclass:: black.trans.StringTransformer - :show-inheritance: - :members: - -:class:`CustomSplit` ----------------------------- - -.. autoclass:: black.trans.CustomSplit - :members: - -:class:`CustomSplitMapMixin` ------------------------------ - -.. autoclass:: black.trans.CustomSplitMapMixin - :show-inheritance: - :members: - -:class:`StringMerger` ----------------------- - -.. autoclass:: black.trans.StringMerger - :show-inheritance: - :members: - -:class:`StringParenStripper` ------------------------------ - -.. autoclass:: black.trans.StringParenStripper - :show-inheritance: - :members: - -:class:`BaseStringSplitter` ------------------------------ - -.. autoclass:: black.trans.BaseStringSplitter - :show-inheritance: - :members: - -:class:`StringSplitter` ------------------------------ - -.. autoclass:: black.trans.StringSplitter - :show-inheritance: - :members: - -:class:`StringParenWrapper` ------------------------------ - -.. autoclass:: black.trans.StringParenWrapper - :show-inheritance: - :members: - -:class:`StringParser` ------------------------------ - -.. autoclass:: black.trans.StringParser - :members: - -:class:`DebugVisitor` ------------------------- - -.. autoclass:: black.debug.DebugVisitor - :show-inheritance: - :members: - -:class:`Replacement` ------------------------- - -.. autoclass:: black.handle_ipynb_magics.Replacement - :members: - -:class:`CellMagic` ------------------------- - -.. autoclass:: black.handle_ipynb_magics.CellMagic - :members: - -:class:`CellMagicFinder` ------------------------- - -.. autoclass:: black.handle_ipynb_magics.CellMagicFinder - :show-inheritance: - :members: - -:class:`OffsetAndMagic` ------------------------- - -.. autoclass:: black.handle_ipynb_magics.OffsetAndMagic - :members: - -:class:`MagicFinder` ------------------------- - -.. autoclass:: black.handle_ipynb_magics.MagicFinder - :show-inheritance: - :members: - -:class:`Cache` ------------------------- - -.. autoclass:: black.cache.Cache - :show-inheritance: - :members: - -Enum Classes -~~~~~~~~~~~~~ - -Classes inherited from Python `Enum `_ class. - -:class:`Changed` ----------------- - -.. autoclass:: black.report.Changed - :show-inheritance: - :members: - -:class:`WriteBack` ------------------- - -.. autoclass:: black.WriteBack - :show-inheritance: - :members: - -:class:`TargetVersion` ----------------------- - -.. autoclass:: black.mode.TargetVersion - :show-inheritance: - :members: - -:class:`Feature` ------------------- - -.. autoclass:: black.mode.Feature - :show-inheritance: - :members: - -:class:`Preview` ------------------- - -.. autoclass:: black.mode.Preview - :show-inheritance: - :members: diff --git a/docs/contributing/reference/reference_exceptions.rst b/docs/contributing/reference/reference_exceptions.rst deleted file mode 100644 index ab46ebdb628..00000000000 --- a/docs/contributing/reference/reference_exceptions.rst +++ /dev/null @@ -1,18 +0,0 @@ -*Black* exceptions -================== - -*Contents are subject to change.* - -.. currentmodule:: black - -.. autoexception:: black.trans.CannotTransform - -.. autoexception:: black.linegen.CannotSplit - -.. autoexception:: black.brackets.BracketMatchError - -.. autoexception:: black.report.NothingChanged - -.. autoexception:: black.parsing.InvalidInput - -.. autoexception:: black.mode.Deprecated diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst deleted file mode 100644 index ebadf6975a7..00000000000 --- a/docs/contributing/reference/reference_functions.rst +++ /dev/null @@ -1,172 +0,0 @@ -*Black* functions -================= - -*Contents are subject to change.* - -.. currentmodule:: black - -Assertions and checks ---------------------- - -.. autofunction:: black.assert_equivalent - -.. autofunction:: black.assert_stable - -.. autofunction:: black.lines.can_be_split - -.. autofunction:: black.lines.can_omit_invisible_parens - -.. autofunction:: black.nodes.is_empty_tuple - -.. autofunction:: black.nodes.is_import - -.. autofunction:: black.lines.is_line_short_enough - -.. autofunction:: black.nodes.is_multiline_string - -.. autofunction:: black.nodes.is_one_tuple - -.. autofunction:: black.brackets.is_split_after_delimiter - -.. autofunction:: black.brackets.is_split_before_delimiter - -.. autofunction:: black.nodes.is_stub_body - -.. autofunction:: black.nodes.is_stub_suite - -.. autofunction:: black.nodes.is_vararg - -.. autofunction:: black.nodes.is_yield - - -Formatting ----------- - -.. autofunction:: black.format_file_contents - -.. autofunction:: black.format_file_in_place - -.. autofunction:: black.format_stdin_to_stdout - -.. autofunction:: black.format_str - -.. autofunction:: black.reformat_one - -.. autofunction:: black.concurrency.schedule_formatting - -File operations ---------------- - -.. autofunction:: black.dump_to_file - -.. autofunction:: black.find_project_root - -.. autofunction:: black.gen_python_files - -.. autofunction:: black.read_pyproject_toml - -Parsing -------- - -.. autofunction:: black.decode_bytes - -.. autofunction:: black.parsing.lib2to3_parse - -.. autofunction:: black.parsing.lib2to3_unparse - -Split functions ---------------- - -.. autofunction:: black.linegen.bracket_split_build_line - -.. autofunction:: black.linegen.bracket_split_succeeded_or_raise - -.. autofunction:: black.linegen.delimiter_split - -.. autofunction:: black.linegen.left_hand_split - -.. autofunction:: black.linegen.right_hand_split - -.. autofunction:: black.linegen.standalone_comment_split - -.. autofunction:: black.linegen.transform_line - -Caching -------- - -.. autofunction:: black.cache.get_cache_dir - -.. autofunction:: black.cache.get_cache_file - -Utilities ---------- - -.. py:function:: black.debug.DebugVisitor.show(code: str) -> None - - Pretty-print the lib2to3 AST of a given string of `code`. - -.. autofunction:: black.concurrency.cancel - -.. autofunction:: black.nodes.child_towards - -.. autofunction:: black.nodes.container_of - -.. autofunction:: black.comments.convert_one_fmt_off_pair - -.. autofunction:: black.diff - -.. autofunction:: black.linegen.dont_increase_indentation - -.. autofunction:: black.numerics.format_float_or_int_string - -.. autofunction:: black.nodes.ensure_visible - -.. autofunction:: black.lines.enumerate_reversed - -.. autofunction:: black.comments.generate_comments - -.. autofunction:: black.comments.generate_ignored_nodes - -.. autofunction:: black.comments.is_fmt_on - -.. autofunction:: black.comments.children_contains_fmt_on - -.. autofunction:: black.nodes.first_leaf_of - -.. autofunction:: black.linegen.generate_trailers_to_omit - -.. autofunction:: black.get_future_imports - -.. autofunction:: black.comments.list_comments - -.. autofunction:: black.comments.make_comment - -.. autofunction:: black.linegen.maybe_make_parens_invisible_in_atom - -.. autofunction:: black.brackets.max_delimiter_priority_in_atom - -.. autofunction:: black.normalize_fmt_off - -.. autofunction:: black.numerics.normalize_numeric_literal - -.. autofunction:: black.comments.normalize_trailing_prefix - -.. autofunction:: black.strings.normalize_string_prefix - -.. autofunction:: black.strings.normalize_string_quotes - -.. autofunction:: black.linegen.normalize_invisible_parens - -.. autofunction:: black.nodes.preceding_leaf - -.. autofunction:: black.re_compile_maybe_verbose - -.. autofunction:: black.linegen.should_split_line - -.. autofunction:: black.concurrency.shutdown - -.. autofunction:: black.strings.sub_twice - -.. autofunction:: black.nodes.whitespace - -.. autofunction:: black.nodes.make_simple_prefix diff --git a/docs/contributing/reference/reference_summary.rst b/docs/contributing/reference/reference_summary.rst deleted file mode 100644 index c6163d897b6..00000000000 --- a/docs/contributing/reference/reference_summary.rst +++ /dev/null @@ -1,19 +0,0 @@ -Developer reference -=================== - -.. note:: - - As of June 2023, the documentation of *Black classes* and *Black exceptions* - has been updated to the latest available version. - - The documentation of *Black functions* is quite outdated and has been neglected. Many - functions worthy of inclusion aren't documented. Contributions are appreciated! - -*Contents are subject to change.* - -.. toctree:: - :maxdepth: 2 - - reference_classes - reference_functions - reference_exceptions From 17f7f297efd29a9b4187af8420e88ca156c1d221 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:41:45 -0800 Subject: [PATCH 690/700] Simplify code in lines.py (#4167) This has been getting a little messy. These changes neaten things up, we don't have to keep guarding against `self.previous_line is not None`, we make it clearer what logic has side effects, we reduce the amount of code that tricky `before` could touch, etc --- src/black/lines.py | 62 +++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index 29f87137614..72634fd36d2 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -565,14 +565,9 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: ) before, after = self._maybe_empty_lines(current_line) previous_after = self.previous_block.after if self.previous_block else 0 - before = ( - # Black should not insert empty lines at the beginning - # of the file - 0 - if self.previous_line is None - else before - previous_after - ) + before = max(0, before - previous_after) if ( + # Always have one empty line after a module docstring self.previous_block and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 @@ -607,10 +602,11 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: self.previous_block = block return block - def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: + def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: # noqa: C901 max_allowed = 1 if current_line.depth == 0: max_allowed = 1 if self.mode.is_pyi else 2 + if current_line.leaves: # Consume the first leaf's extra newlines. first_leaf = current_line.leaves[0] @@ -623,9 +619,22 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: user_had_newline = bool(before) depth = current_line.depth + # Mutate self.previous_defs, remainder of this function should be pure previous_def = None while self.previous_defs and self.previous_defs[-1].depth >= depth: previous_def = self.previous_defs.pop() + if current_line.is_def or current_line.is_class: + self.previous_defs.append(current_line) + + if self.previous_line is None: + # Don't insert empty lines before the first line in the file. + return 0, 0 + + if current_line.is_docstring: + if self.previous_line.is_class: + return 0, 1 + if self.previous_line.opens_block and self.previous_line.is_def: + return 0, 0 if previous_def is not None: assert self.previous_line is not None @@ -668,49 +677,24 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: ) if ( - self.previous_line - and self.previous_line.is_import + self.previous_line.is_import and not current_line.is_import and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import) and depth == self.previous_line.depth ): return (before or 1), 0 - if ( - self.previous_line - and self.previous_line.is_class - and current_line.is_docstring - ): - return 0, 1 - - # In preview mode, always allow blank lines, except right before a function - # docstring - is_empty_first_line_ok = not current_line.is_docstring or ( - self.previous_line and not self.previous_line.is_def - ) - - if ( - self.previous_line - and self.previous_line.opens_block - and not is_empty_first_line_ok - ): - return 0, 0 return before, 0 def _maybe_empty_lines_for_class_or_def( # noqa: C901 self, current_line: Line, before: int, user_had_newline: bool ) -> Tuple[int, int]: - if not current_line.is_decorator: - self.previous_defs.append(current_line) - if self.previous_line is None: - # Don't insert empty lines before the first line in the file. - return 0, 0 + assert self.previous_line is not None if self.previous_line.is_decorator: if self.mode.is_pyi and current_line.is_stub_class: # Insert an empty line after a decorated stub class return 0, 1 - return 0, 0 if self.previous_line.depth < current_line.depth and ( @@ -718,8 +702,7 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 ): if self.mode.is_pyi: return 0, 0 - else: - return 1 if user_had_newline else 0, 0 + return 1 if user_had_newline else 0, 0 comment_to_add_newlines: Optional[LinesBlock] = None if ( @@ -750,9 +733,6 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 newlines = 0 else: newlines = 1 - # Remove case `self.previous_line.depth > current_line.depth` below when - # this becomes stable. - # # Don't inspect the previous line if it's part of the body of the previous # statement in the same level, we always want a blank line if there's # something with a body preceding. @@ -769,8 +749,6 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 # 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: From 7d789469ed947022f183962b823f5862511272ac Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:15:18 -0800 Subject: [PATCH 691/700] Describe 2024 module docstring more accurately (#4168) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2fe14cd5246..95ef0095102 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ changes: - Hex codes in Unicode escape sequences are now standardized to lowercase (#2916) - Allow empty first lines at the beginning of most blocks (#3967, #4061) - Add parentheses around long type annotations (#3899) -- Standardize on a single newline after module docstrings (#3932) +- Enforce newline after module docstrings (#3932, #4028) - Fix incorrect magic trailing comma handling in return types (#3916) - Remove blank lines before class docstrings (#3692) - Wrap multiple context managers in parentheses if combined in a single `with` statement From bccec8adfbed2bbc24c0859e8758d5e7809d42b7 Mon Sep 17 00:00:00 2001 From: Daniel Krzeminski Date: Thu, 25 Jan 2024 18:41:37 -0600 Subject: [PATCH 692/700] Show warning on invalid toml configuration (#4165) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 1 + src/black/__init__.py | 17 +++++++++++++++++ tests/data/incorrect_spelling.toml | 5 +++++ tests/test_black.py | 17 +++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 tests/data/incorrect_spelling.toml diff --git a/CHANGES.md b/CHANGES.md index 95ef0095102..45cb967e74a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -62,6 +62,7 @@ release: +- Print warning when toml config contains an invalid key (#4165) - Fix symlink handling, properly catch and ignore symlinks that point outside of root (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) diff --git a/src/black/__init__.py b/src/black/__init__.py index e3cbaab5f1d..961ed9479a8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -142,6 +142,7 @@ def read_pyproject_toml( if not config: return None else: + spellcheck_pyproject_toml_keys(ctx, list(config), value) # Sanitize the values to be Click friendly. For more information please see: # https://github.com/psf/black/issues/1458 # https://github.com/pallets/click/issues/1567 @@ -181,6 +182,22 @@ def read_pyproject_toml( return value +def spellcheck_pyproject_toml_keys( + ctx: click.Context, config_keys: List[str], config_file_path: str +) -> None: + invalid_keys: List[str] = [] + available_config_options = {param.name for param in ctx.command.params} + for key in config_keys: + if key not in available_config_options: + invalid_keys.append(key) + if invalid_keys: + keys_str = ", ".join(map(repr, invalid_keys)) + out( + f"Invalid config keys detected: {keys_str} (in {config_file_path})", + fg="red", + ) + + def target_version_option_callback( c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...] ) -> List[TargetVersion]: diff --git a/tests/data/incorrect_spelling.toml b/tests/data/incorrect_spelling.toml new file mode 100644 index 00000000000..560c9e27be2 --- /dev/null +++ b/tests/data/incorrect_spelling.toml @@ -0,0 +1,5 @@ +[tool.black] +ine_length = 50 +target-ersion = ['py37'] +exclude='\.pyi?$' +include='\.py?$' \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 2b5fab5d28d..a979a95b674 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -106,6 +106,7 @@ class FakeContext(click.Context): def __init__(self) -> None: self.default_map: Dict[str, Any] = {} self.params: Dict[str, Any] = {} + self.command: click.Command = black.main # Dummy root, since most of the tests don't care about it self.obj: Dict[str, Any] = {"root": PROJECT_ROOT} @@ -1538,6 +1539,22 @@ def test_parse_pyproject_toml(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + def test_spellcheck_pyproject_toml(self) -> None: + test_toml_file = THIS_DIR / "data" / "incorrect_spelling.toml" + result = BlackRunner().invoke( + black.main, + [ + "--code=print('hello world')", + "--verbose", + f"--config={str(test_toml_file)}", + ], + ) + + assert ( + r"Invalid config keys detected: 'ine_length', 'target_ersion' (in" + rf" {test_toml_file})" in result.stderr + ) + def test_parse_pyproject_toml_project_metadata(self) -> None: for test_toml, expected in [ ("only_black_pyproject.toml", ["py310"]), From 4f47cac1925a2232892ceae438e2c62f81517714 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 17:00:47 -0800 Subject: [PATCH 693/700] Add --unstable flag (#4096) --- CHANGES.md | 17 ++ docs/faq.md | 7 +- docs/the_black_code_style/current_style.md | 6 + docs/the_black_code_style/future_style.md | 217 +++++++++--------- docs/the_black_code_style/index.md | 8 +- .../black_as_a_server.md | 6 + docs/usage_and_configuration/the_basics.md | 31 ++- pyproject.toml | 9 +- src/black/__init__.py | 49 +++- src/black/mode.py | 39 ++-- src/blackd/__init__.py | 104 +++++---- tests/data/cases/preview_cantfit.py | 14 -- tests/data/cases/preview_cantfit_string.py | 18 ++ tests/data/cases/preview_comments7.py | 2 +- tests/data/cases/preview_long_dict_values.py | 2 +- tests/data/cases/preview_long_strings.py | 2 +- .../preview_long_strings__east_asian_width.py | 2 +- .../cases/preview_long_strings__edge_case.py | 2 +- .../cases/preview_long_strings__regression.py | 2 +- tests/data/cases/preview_multiline_strings.py | 2 +- ...eview_return_annotation_brackets_string.py | 2 +- tests/test_black.py | 63 ++--- tests/util.py | 16 +- 23 files changed, 368 insertions(+), 252 deletions(-) create mode 100644 tests/data/cases/preview_cantfit_string.py diff --git a/CHANGES.md b/CHANGES.md index 45cb967e74a..0496603e2c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,14 @@ changes: - Fix incorrect formatting of certain async statements (#3609) - Allow combining `# fmt: skip` with other comments (#3959) +There are already a few improvements in the `--preview` style, which are slated for the +2025 stable style. Try them out and +[share your feedback](https://github.com/psf/black/issues). In the past, the preview +style has included some features that we were not able to stabilize. This year, we're +adding a separate `--unstable` style for features with known problems. Now, the +`--preview` style only includes features that we actually expect to make it into next +year's stable style. + ### Stable style @@ -53,6 +61,12 @@ release: +- Add `--unstable` style, covering preview features that have known problems that would + block them from going into the stable style. Also add the `--enable-unstable-feature` + flag; for example, use + `--enable-unstable-feature hug_parens_with_braces_and_square_brackets` to apply this + preview style throughout 2024, even if a later Black release downgrades the feature to + unstable (#4096) - Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) @@ -66,6 +80,9 @@ release: - Fix symlink handling, properly catch and ignore symlinks that point outside of root (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) +- Remove the long-deprecated `--experimental-string-processing` flag. This feature can + currently be enabled with `--preview --enable-unstable-feature string_processing`. + (#4096) ### Packaging diff --git a/docs/faq.md b/docs/faq.md index c62e1b504b5..124a096efac 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -41,9 +41,10 @@ other tools, such as `# noqa`, may be moved by _Black_. See below for more detai 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.md) 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 -styles, using the `--preview` flag. +Starting in 2022, the formatting output is stable for the releases made in the same year +(other than unintentional bugs). At the beginning of every year, the first release will +make changes to the stable style. It is possible to opt in to the latest formatting +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 00bd81416dc..ca5d1d4a701 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -449,6 +449,12 @@ file that are not enforced yet but might be in a future version of the formatter _Black_ will normalize line endings (`\n` or `\r\n`) based on the first line ending of the file. +### Form feed characters + +_Black_ will retain form feed characters on otherwise empty lines at the module level. +Only one form feed is retained for a group of consecutive empty lines. Where there are +two empty lines in a row, the form feed is placed on the second line. + ## Pragmatism Early versions of _Black_ used to be absolutist in some respects. They took after its diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index f55ea5f60a9..1cdd25fdb7c 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -1,54 +1,5 @@ # The (future of the) Black code style -```{warning} -Changes to this document often aren't tied and don't relate to releases of -_Black_. It's recommended that you read the latest version available. -``` - -## Using backslashes for with statements - -[Backslashes are bad and should be never be used](labels/why-no-backslashes) however -there is one exception: `with` statements using multiple context managers. Before Python -3.9 Python's grammar does not allow organizing parentheses around the series of context -managers. - -We don't want formatting like: - -```py3 -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - ... # nothing to split on - line too long -``` - -So _Black_ will, when we implement this, format it like this: - -```py3 -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - ... # backslashes and an ugly stranded colon -``` - -Although when the target version is Python 3.9 or higher, _Black_ uses parentheses -instead in `--preview` mode (see below) since they're allowed in Python 3.9 and higher. - -An alternative to consider if the backslashes in the above formatting are undesirable is -to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the -following way: - -```python -with contextlib.ExitStack() as exit_stack: - cm1 = exit_stack.enter_context(make_context_manager1()) - cm2 = exit_stack.enter_context(make_context_manager2()) - cm3 = exit_stack.enter_context(make_context_manager3()) - cm4 = exit_stack.enter_context(make_context_manager4()) - ... -``` - -(labels/preview-style)= - ## Preview style Experimental, potentially disruptive style changes are gathered under the `--preview` @@ -56,62 +7,38 @@ CLI flag. At the end of each year, these changes may be adopted into the default as described in [The Black Code Style](index.md). 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). - -### Improved line breaks - -For assignment expressions, _Black_ now prefers to split and wrap the right side of the -assignment instead of left side. For example: - -```python -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument -) -``` - -will be changed to: +In the past, the preview style included some features with known bugs, so that we were +unable to move these features to the stable style. Therefore, such features are now +moved to the `--unstable` style. All features in the `--preview` style are expected to +make it to next year's stable style; features in the `--unstable` style will be +stabilized only if issues with them are fixed. If bugs are discovered in a `--preview` +feature, it is demoted to the `--unstable` style. To avoid thrash when a feature is +demoted from the `--preview` to the `--unstable` style, users can use the +`--enable-unstable-feature` flag to enable specific unstable features. -```python -some_dict["with_a_long_key"] = ( - some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument - ) -) -``` +Currently, the following features are included in the preview style: -### Improved parentheses management +- `hex_codes_in_unicode_sequences`: normalize casing of Unicode escape characters in + strings +- `unify_docstring_detection`: fix inconsistencies in whether certain strings are + detected as docstrings +- `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested + brackets ([see below](labels/hug-parens)) +- `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no + longer normalized -For dict literals with long values, they are now wrapped in parentheses. Unnecessary -parentheses are now removed. For example: +(labels/unstable-features)= -```python -my_dict = { - "a key in my dict": a_very_long_variable - * and_a_very_long_function_call() - / 100000.0, - "another key": (short_value), -} -``` +The unstable style additionally includes the following features: -will be changed to: +- `string_processing`: split long string literals and related changes + ([see below](labels/string-processing)) +- `wrap_long_dict_values_in_parens`: add parentheses to long values in dictionaries + ([see below](labels/wrap-long-dict-values)) +- `multiline_string_handling`: more compact formatting of expressions involving + multiline strings ([see below](labels/multiline-string-handling)) -```python -my_dict = { - "a key in my dict": ( - a_very_long_variable * and_a_very_long_function_call() / 100000.0 - ), - "another key": short_value, -} -``` +(labels/hug-parens)= ### Improved multiline dictionary and list indentation for sole function parameter @@ -185,6 +112,46 @@ foo( ) ``` +(labels/string-processing)= + +### 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). + +(labels/wrap-long-dict-values)= + +### Improved parentheses management in dicts + +For dict literals with long values, they are now wrapped in parentheses. Unnecessary +parentheses are now removed. For example: + +```python +my_dict = { + "a key in my dict": a_very_long_variable + * and_a_very_long_function_call() + / 100000.0, + "another key": (short_value), +} +``` + +will be changed to: + +```python +my_dict = { + "a key in my dict": ( + a_very_long_variable * and_a_very_long_function_call() / 100000.0 + ), + "another key": short_value, +} +``` + +(labels/multiline-string-handling)= + ### Improved multiline string handling _Black_ is smarter when formatting multiline strings, especially in function arguments, @@ -297,13 +264,51 @@ s = ( # Top comment ) ``` -======= +## Potential future changes + +This section lists changes that we may want to make in the future, but that aren't +implemented yet. + +### Using backslashes for with statements + +[Backslashes are bad and should be never be used](labels/why-no-backslashes) however +there is one exception: `with` statements using multiple context managers. Before Python +3.9 Python's grammar does not allow organizing parentheses around the series of context +managers. + +We don't want formatting like: + +```py3 +with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: + ... # nothing to split on - line too long +``` + +So _Black_ will, when we implement this, format it like this: + +```py3 +with \ + make_context_manager1() as cm1, \ + make_context_manager2() as cm2, \ + make_context_manager3() as cm3, \ + make_context_manager4() as cm4 \ +: + ... # backslashes and an ugly stranded colon +``` + +Although when the target version is Python 3.9 or higher, _Black_ uses parentheses +instead in `--preview` mode (see below) since they're allowed in Python 3.9 and higher. -### Form feed characters +An alternative to consider if the backslashes in the above formatting are undesirable is +to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the +following way: -_Black_ will now retain form feed characters on otherwise empty lines at the module -level. Only one form feed is retained for a group of consecutive empty lines. Where -there are two empty lines in a row, the form feed will be placed on the second line. +```python +with contextlib.ExitStack() as exit_stack: + cm1 = exit_stack.enter_context(make_context_manager1()) + cm2 = exit_stack.enter_context(make_context_manager2()) + cm3 = exit_stack.enter_context(make_context_manager3()) + cm4 = exit_stack.enter_context(make_context_manager4()) + ... +``` -_Black_ already retained form feed literals inside a comment or inside a string. This -remains the case. +(labels/preview-style)= diff --git a/docs/the_black_code_style/index.md b/docs/the_black_code_style/index.md index 1719347eec8..58f28673022 100644 --- a/docs/the_black_code_style/index.md +++ b/docs/the_black_code_style/index.md @@ -42,9 +42,11 @@ _Black_: enabled by newer Python language syntax as well as due to improvements in the formatting logic. -- The `--preview` flag is exempt from this policy. There are no guarantees around the - stability of the output with that flag passed into _Black_. This flag is intended for - allowing experimentation with the proposed changes to the _Black_ code style. +- The `--preview` and `--unstable` flags are exempt from this policy. There are no + guarantees around the stability of the output with these flags passed into _Black_. + They are intended for allowing experimentation with proposed changes to the _Black_ + code style. The `--preview` style at the end of a year should closely match the stable + style for the next year, but we may always make changes. Documentation for both the current and future styles can be found: diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index f24fb34d915..0a7edb57fd7 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -62,6 +62,12 @@ The headers controlling how source code is formatted are: - `X-Preview`: corresponds to the `--preview` command line flag. If present and its value is not an empty string, experimental and potentially disruptive style changes will be used. +- `X-Unstable`: corresponds to the `--unstable` command line flag. If present and its + value is not an empty string, experimental style changes that are known to be buggy + will be used. +- `X-Enable-Unstable-Feature`: corresponds to the `--enable-unstable-feature` flag. The + contents of the flag must be a comma-separated list of unstable features to be + enabled. Example: `X-Enable-Unstable-Feature: feature1, feature2`. - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the `--fast` command line flag. - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index b541f07907c..a42e093155b 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -144,9 +144,34 @@ magic trailing comma is ignored. #### `--preview` -Enable potentially disruptive style changes that may be added to Black's main -functionality in the next major release. Read more about -[our preview style](labels/preview-style). +Enable potentially disruptive style changes that we expect to add to Black's main +functionality in the next major release. Use this if you want a taste of what next +year's style will look like. + +Read more about [our preview style](labels/preview-style). + +There is no guarantee on the code style produced by this flag across releases. + +#### `--unstable` + +Enable all style changes in `--preview`, plus additional changes that we would like to +make eventually, but that have known issues that need to be fixed before they can move +back to the `--preview` style. Use this if you want to experiment with these changes and +help fix issues with them. + +There is no guarantee on the code style produced by this flag across releases. + +#### `--enable-unstable-feature` + +Enable specific features from the `--unstable` style. See +[the preview style documentation](labels/unstable-features) for the list of supported +features. This flag can only be used when `--preview` is enabled. Users are encouraged +to use this flag if they use `--preview` style and a feature that affects their code is +moved from the `--preview` to the `--unstable` style, but they want to avoid the thrash +from undoing this change. + +There are no guarantees on the behavior of these features, or even their existence, +across releases. (labels/exit-code)= diff --git a/pyproject.toml b/pyproject.toml index 24b9c07674d..fa3654b8d67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,10 +16,11 @@ extend-exclude = ''' | profiling )/ ''' -# We use preview style for formatting Black itself. If you -# want stable formatting across releases, you should keep -# this off. -preview = true +# We use the unstable style for formatting Black itself. If you +# want bug-free formatting, you should keep this off. If you want +# stable formatting across releases, you should also keep `preview = true` +# (which is implied by this flag) off. +unstable = true # Build system information and other project-specific configuration below. # NOTE: You don't need this in your own Black configuration. diff --git a/src/black/__init__.py b/src/black/__init__.py index 961ed9479a8..ebc7ac8eda5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -68,7 +68,7 @@ from black.lines import EmptyLineTracker, LinesBlock from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature from black.mode import Mode as Mode # re-exported -from black.mode import TargetVersion, supports_feature +from black.mode import Preview, TargetVersion, supports_feature from black.nodes import ( STARS, is_number_token, @@ -209,6 +209,13 @@ def target_version_option_callback( return [TargetVersion[val.upper()] for val in v] +def enable_unstable_feature_callback( + c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...] +) -> List[Preview]: + """Compute the features from an --enable-unstable-feature flag.""" + return [Preview[val] for val in v] + + def re_compile_maybe_verbose(regex: str) -> Pattern[str]: """Compile a regular expression string in `regex`. @@ -303,12 +310,6 @@ def validate_regex( is_flag=True, help="Don't use trailing commas as a reason to split lines.", ) -@click.option( - "--experimental-string-processing", - is_flag=True, - hidden=True, - help="(DEPRECATED and now included in --preview) Normalize string literals.", -) @click.option( "--preview", is_flag=True, @@ -317,6 +318,26 @@ def validate_regex( " functionality in the next major release." ), ) +@click.option( + "--unstable", + is_flag=True, + help=( + "Enable potentially disruptive style changes that have known bugs or are not" + " currently expected to make it into the stable style Black's next major" + " release. Implies --preview." + ), +) +@click.option( + "--enable-unstable-feature", + type=click.Choice([v.name for v in Preview]), + callback=enable_unstable_feature_callback, + multiple=True, + help=( + "Enable specific features included in the `--unstable` style. Requires" + " `--preview`. No compatibility guarantees are provided on the behavior" + " or existence of any unstable features." + ), +) @click.option( "--check", is_flag=True, @@ -507,8 +528,9 @@ def main( # noqa: C901 skip_source_first_line: bool, skip_string_normalization: bool, skip_magic_trailing_comma: bool, - experimental_string_processing: bool, preview: bool, + unstable: bool, + enable_unstable_feature: List[Preview], quiet: bool, verbose: bool, required_version: Optional[str], @@ -534,6 +556,14 @@ def main( # noqa: C901 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") ctx.exit(1) + # It doesn't do anything if --unstable is also passed, so just allow it. + if enable_unstable_feature and not (preview or unstable): + out( + main.get_usage(ctx) + + "\n\n'--enable-unstable-feature' requires '--preview'." + ) + ctx.exit(1) + root, method = ( find_project_root(src, stdin_filename) if code is None else (None, None) ) @@ -595,9 +625,10 @@ def main( # noqa: C901 skip_source_first_line=skip_source_first_line, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, - experimental_string_processing=experimental_string_processing, preview=preview, + unstable=unstable, python_cell_magics=set(python_cell_magics), + enabled_features=set(enable_unstable_feature), ) lines: List[Tuple[int, int]] = [] diff --git a/src/black/mode.py b/src/black/mode.py index a1519f17bcc..68919fb4901 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -9,7 +9,6 @@ from hashlib import sha256 from operator import attrgetter from typing import Dict, Final, Set -from warnings import warn from black.const import DEFAULT_LINE_LENGTH @@ -179,6 +178,16 @@ class Preview(Enum): multiline_string_handling = auto() +UNSTABLE_FEATURES: Set[Preview] = { + # Many issues, see summary in https://github.com/psf/black/issues/4042 + Preview.string_processing, + # See issues #3452 and #4158 + Preview.wrap_long_dict_values_in_parens, + # See issue #4159 + Preview.multiline_string_handling, +} + + class Deprecated(UserWarning): """Visible deprecation warning.""" @@ -192,28 +201,24 @@ class Mode: is_ipynb: bool = False skip_source_first_line: 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: - if self.experimental_string_processing: - warn( - "`experimental string processing` has been included in `preview`" - " and deprecated. Use `preview` instead.", - Deprecated, - ) + unstable: bool = False + enabled_features: Set[Preview] = field(default_factory=set) def __contains__(self, feature: Preview) -> bool: """ Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag. - The argument is not checked and features are not differentiated. - They only exist to make development easier by clarifying intent. + In unstable mode, all features are enabled. In preview mode, all features + except those in UNSTABLE_FEATURES are enabled. Any features in + `self.enabled_features` are also enabled. """ - if feature is Preview.string_processing: - return self.preview or self.experimental_string_processing - return self.preview + if self.unstable: + return True + if feature in self.enabled_features: + return True + return self.preview and feature not in UNSTABLE_FEATURES def get_cache_key(self) -> str: if self.target_versions: @@ -231,7 +236,9 @@ def get_cache_key(self) -> str: str(int(self.is_ipynb)), str(int(self.skip_source_first_line)), str(int(self.magic_trailing_comma)), - str(int(self.experimental_string_processing)), + sha256( + (",".join(sorted(f.name for f in self.enabled_features))).encode() + ).hexdigest(), str(int(self.preview)), sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), ] diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 6b0f3d33295..7041671f596 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -8,6 +8,7 @@ try: from aiohttp import web + from multidict import MultiMapping from .middlewares import cors except ImportError as ie: @@ -34,6 +35,8 @@ SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" PREVIEW = "X-Preview" +UNSTABLE = "X-Unstable" +ENABLE_UNSTABLE_FEATURE = "X-Enable-Unstable-Feature" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" DIFF_HEADER = "X-Diff" @@ -45,6 +48,8 @@ SKIP_STRING_NORMALIZATION_HEADER, SKIP_MAGIC_TRAILING_COMMA, PREVIEW, + UNSTABLE, + ENABLE_UNSTABLE_FEATURE, FAST_OR_SAFE_HEADER, DIFF_HEADER, ] @@ -53,6 +58,10 @@ BLACK_VERSION_HEADER = "X-Black-Version" +class HeaderError(Exception): + pass + + class InvalidVariantHeader(Exception): pass @@ -93,55 +102,21 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: return web.Response( status=501, text="This server only supports protocol version 1" ) - try: - line_length = int( - request.headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH) - ) - except ValueError: - return web.Response(status=400, text="Invalid line length header value") - if PYTHON_VARIANT_HEADER in request.headers: - value = request.headers[PYTHON_VARIANT_HEADER] - try: - pyi, versions = parse_python_variant_header(value) - except InvalidVariantHeader as e: - return web.Response( - status=400, - text=f"Invalid value for {PYTHON_VARIANT_HEADER}: {e.args[0]}", - ) - else: - pyi = False - versions = set() - - skip_string_normalization = bool( - request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False) - ) - skip_magic_trailing_comma = bool( - request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False) - ) - skip_source_first_line = bool( - request.headers.get(SKIP_SOURCE_FIRST_LINE, False) - ) - preview = bool(request.headers.get(PREVIEW, False)) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True - mode = black.FileMode( - target_versions=versions, - is_pyi=pyi, - line_length=line_length, - skip_source_first_line=skip_source_first_line, - string_normalization=not skip_string_normalization, - magic_trailing_comma=not skip_magic_trailing_comma, - preview=preview, - ) + try: + mode = parse_mode(request.headers) + except HeaderError as e: + return web.Response(status=400, text=e.args[0]) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" req_str = req_bytes.decode(charset) then = datetime.now(timezone.utc) header = "" - if skip_source_first_line: + if mode.skip_source_first_line: first_newline_position: int = req_str.find("\n") + 1 header = req_str[:first_newline_position] req_str = req_str[first_newline_position:] @@ -190,6 +165,57 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: return web.Response(status=500, headers=headers, text=str(e)) +def parse_mode(headers: MultiMapping[str]) -> black.Mode: + try: + line_length = int(headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)) + except ValueError: + raise HeaderError("Invalid line length header value") from None + + if PYTHON_VARIANT_HEADER in headers: + value = headers[PYTHON_VARIANT_HEADER] + try: + pyi, versions = parse_python_variant_header(value) + except InvalidVariantHeader as e: + raise HeaderError( + f"Invalid value for {PYTHON_VARIANT_HEADER}: {e.args[0]}", + ) from None + else: + pyi = False + versions = set() + + skip_string_normalization = bool( + headers.get(SKIP_STRING_NORMALIZATION_HEADER, False) + ) + skip_magic_trailing_comma = bool(headers.get(SKIP_MAGIC_TRAILING_COMMA, False)) + skip_source_first_line = bool(headers.get(SKIP_SOURCE_FIRST_LINE, False)) + + preview = bool(headers.get(PREVIEW, False)) + unstable = bool(headers.get(UNSTABLE, False)) + enable_features: Set[black.Preview] = set() + enable_unstable_features = headers.get(ENABLE_UNSTABLE_FEATURE, "").split(",") + for piece in enable_unstable_features: + piece = piece.strip() + if piece: + try: + enable_features.add(black.Preview[piece]) + except KeyError: + raise HeaderError( + f"Invalid value for {ENABLE_UNSTABLE_FEATURE}: {piece}", + ) from None + + return black.FileMode( + target_versions=versions, + is_pyi=pyi, + line_length=line_length, + skip_source_first_line=skip_source_first_line, + string_normalization=not skip_string_normalization, + magic_trailing_comma=not skip_magic_trailing_comma, + preview=preview, + unstable=unstable, + enabled_features=enable_features, + ) + + def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersion]]: if value == "pyi": return True, set() diff --git a/tests/data/cases/preview_cantfit.py b/tests/data/cases/preview_cantfit.py index d5da6654f0c..29789c7e653 100644 --- a/tests/data/cases/preview_cantfit.py +++ b/tests/data/cases/preview_cantfit.py @@ -20,12 +20,6 @@ normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 ) -# long arguments -normal_name = normal_function_name( - "but with super long string arguments that on their own exceed the line limit so there's no way it can ever fit", - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs", - this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, -) string_variable_name = ( "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa ) @@ -78,14 +72,6 @@ [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 ) ) -# long arguments -normal_name = normal_function_name( - "but with super long string arguments that on their own exceed the line limit so" - " there's no way it can ever fit", - "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" - " with spam and eggs and spam with eggs", - this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, -) string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa for key in """ hostname diff --git a/tests/data/cases/preview_cantfit_string.py b/tests/data/cases/preview_cantfit_string.py new file mode 100644 index 00000000000..3b48e318ade --- /dev/null +++ b/tests/data/cases/preview_cantfit_string.py @@ -0,0 +1,18 @@ +# flags: --unstable +# long arguments +normal_name = normal_function_name( + "but with super long string arguments that on their own exceed the line limit so there's no way it can ever fit", + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs", + this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, +) + +# output + +# long arguments +normal_name = normal_function_name( + "but with super long string arguments that on their own exceed the line limit so" + " there's no way it can ever fit", + "eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" + " with spam and eggs and spam with eggs", + this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, +) diff --git a/tests/data/cases/preview_comments7.py b/tests/data/cases/preview_comments7.py index 006d4f7266f..e4d547138db 100644 --- a/tests/data/cases/preview_comments7.py +++ b/tests/data/cases/preview_comments7.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable from .config import ( Any, Bool, diff --git a/tests/data/cases/preview_long_dict_values.py b/tests/data/cases/preview_long_dict_values.py index 54da76038dc..a19210605f6 100644 --- a/tests/data/cases/preview_long_dict_values.py +++ b/tests/data/cases/preview_long_dict_values.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable my_dict = { "something_something": r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 19ac47a7032..86fa1b0c7e1 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." diff --git a/tests/data/cases/preview_long_strings__east_asian_width.py b/tests/data/cases/preview_long_strings__east_asian_width.py index d190f422a60..022b0452522 100644 --- a/tests/data/cases/preview_long_strings__east_asian_width.py +++ b/tests/data/cases/preview_long_strings__east_asian_width.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable # The following strings do not have not-so-many chars, but are long enough # when these are rendered in a monospace font (if the renderer respects # Unicode East Asian Width properties). diff --git a/tests/data/cases/preview_long_strings__edge_case.py b/tests/data/cases/preview_long_strings__edge_case.py index a8e8971968c..28497e731bc 100644 --- a/tests/data/cases/preview_long_strings__edge_case.py +++ b/tests/data/cases/preview_long_strings__edge_case.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable some_variable = "This string is long but not so long that it needs to be split just yet" some_variable = 'This string is long but not so long that it needs to be split just yet' some_variable = "This string is long, just long enough that it needs to be split, u get?" diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 5e76a8cf61c..afe2b311cf4 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable class A: def foo(): result = type(message)("") diff --git a/tests/data/cases/preview_multiline_strings.py b/tests/data/cases/preview_multiline_strings.py index 3ff643610b7..9288f6991bd 100644 --- a/tests/data/cases/preview_multiline_strings.py +++ b/tests/data/cases/preview_multiline_strings.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable """cow say""", call(3, "dogsay", textwrap.dedent("""dove diff --git a/tests/data/cases/preview_return_annotation_brackets_string.py b/tests/data/cases/preview_return_annotation_brackets_string.py index fea0ea6839a..2f937cf54ef 100644 --- a/tests/data/cases/preview_return_annotation_brackets_string.py +++ b/tests/data/cases/preview_return_annotation_brackets_string.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable # Long string example def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]": pass diff --git a/tests/test_black.py b/tests/test_black.py index a979a95b674..6dbe25a90b6 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -158,13 +158,11 @@ def test_empty_ff(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_one_empty_line(self) -> None: - mode = black.Mode(preview=True) for nl in ["\n", "\r\n"]: source = expected = nl - assert_format(source, expected, mode=mode) + assert_format(source, expected) def test_one_empty_line_ff(self) -> None: - mode = black.Mode(preview=True) for nl in ["\n", "\r\n"]: expected = nl tmp_file = Path(black.dump_to_file(nl)) @@ -175,20 +173,13 @@ def test_one_empty_line_ff(self) -> None: with open(tmp_file, "wb") as f: f.write(nl.encode("utf-8")) try: - self.assertFalse( - ff(tmp_file, mode=mode, write_back=black.WriteBack.YES) - ) + self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES)) with open(tmp_file, "rb") as f: actual = f.read().decode("utf-8") finally: 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_from_file( PROJECT_ROOT / "src/black/__init__.py" @@ -252,21 +243,6 @@ def test_piping_diff_with_color(self) -> None: self.assertIn("\033[31m", actual) self.assertIn("\033[0m", actual) - @patch("black.dump_to_file", dump_to_stderr) - def _test_wip(self) -> None: - source, expected = read_data("miscellaneous", "wip") - sys.settrace(tracefunc) - mode = replace( - DEFAULT_MODE, - experimental_string_processing=False, - target_versions={black.TargetVersion.PY38}, - ) - actual = fs(source, mode=mode) - sys.settrace(None) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - def test_pep_572_version_detection(self) -> None: source, _ = read_data("cases", "pep_572") root = black.lib2to3_parse(source) @@ -374,7 +350,7 @@ def test_detect_debug_f_strings(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("miscellaneous", "string_quotes") - mode = black.Mode(preview=True) + mode = black.Mode(unstable=True) assert_format(source, expected, mode) mode = replace(mode, string_normalization=False) not_normalized = fs(source, mode=mode) @@ -1052,7 +1028,6 @@ def test_format_file_contents(self) -> None: black.format_file_contents(invalid, mode=mode, fast=False) self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can") - mode = black.Mode(preview=True) just_crlf = "\r\n" with self.assertRaises(black.NothingChanged): black.format_file_contents(just_crlf, mode=mode, fast=False) @@ -1396,7 +1371,6 @@ def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: return get_output - mode = black.Mode(preview=True) for content, expected in cases: output = io.StringIO() io_TextIOWrapper = io.TextIOWrapper @@ -1407,26 +1381,27 @@ def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: fast=True, content=content, write_back=black.WriteBack.YES, - mode=mode, + mode=DEFAULT_MODE, ) except io.UnsupportedOperation: pass # StringIO does not support detach assert output.getvalue() == expected - # An empty string is the only test case for `preview=False` - output = io.StringIO() - io_TextIOWrapper = io.TextIOWrapper - with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): - try: - black.format_stdin_to_stdout( - fast=True, - content="", - write_back=black.WriteBack.YES, - mode=DEFAULT_MODE, - ) - except io.UnsupportedOperation: - pass # StringIO does not support detach - assert output.getvalue() == "" + def test_cli_unstable(self) -> None: + self.invokeBlack(["--unstable", "-c", "0"], exit_code=0) + self.invokeBlack(["--preview", "-c", "0"], exit_code=0) + # Must also pass --preview + self.invokeBlack( + ["--enable-unstable-feature", "string_processing", "-c", "0"], exit_code=1 + ) + self.invokeBlack( + ["--preview", "--enable-unstable-feature", "string_processing", "-c", "0"], + exit_code=0, + ) + self.invokeBlack( + ["--unstable", "--enable-unstable-feature", "string_processing", "-c", "0"], + exit_code=0, + ) def test_invalid_cli_regex(self) -> None: for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: diff --git a/tests/util.py b/tests/util.py index 9ea30e62fe3..d5425f1f743 100644 --- a/tests/util.py +++ b/tests/util.py @@ -112,16 +112,24 @@ def assert_format( # For both preview and non-preview tests, ensure that Black doesn't crash on # this code, but don't pass "expected" because the precise output may differ. try: + if mode.unstable: + new_mode = replace(mode, unstable=False, preview=False) + else: + new_mode = replace(mode, preview=not mode.preview) _assert_format_inner( source, None, - replace(mode, preview=not mode.preview), + new_mode, fast=fast, minimum_version=minimum_version, lines=lines, ) except Exception as e: - text = "non-preview" if mode.preview else "preview" + text = ( + "unstable" + if mode.unstable + else "non-preview" if mode.preview else "preview" + ) raise FormatFailure( f"Black crashed formatting this case in {text} mode." ) from e @@ -138,7 +146,7 @@ def assert_format( _assert_format_inner( source, None, - replace(mode, preview=preview_mode, line_length=1), + replace(mode, preview=preview_mode, line_length=1, unstable=False), fast=fast, minimum_version=minimum_version, lines=lines, @@ -241,6 +249,7 @@ def get_flags_parser() -> argparse.ArgumentParser: "--skip-magic-trailing-comma", default=False, action="store_true" ) parser.add_argument("--preview", default=False, action="store_true") + parser.add_argument("--unstable", default=False, action="store_true") parser.add_argument("--fast", default=False, action="store_true") parser.add_argument( "--minimum-version", @@ -278,6 +287,7 @@ def parse_mode(flags_line: str) -> TestCaseArgs: is_ipynb=args.ipynb, magic_trailing_comma=not args.skip_magic_trailing_comma, preview=args.preview, + unstable=args.unstable, ) if args.line_ranges: lines = parse_line_ranges(args.line_ranges) From 0e6e46b9eb45f5a22062fe84c2c2ff46bd0d738e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 20:35:21 -0800 Subject: [PATCH 694/700] Prepare release 24.1.0 (#4170) --- CHANGES.md | 46 +++------------------ docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- scripts/release.py | 4 +- 4 files changed, 13 insertions(+), 47 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0496603e2c0..ff921de69eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,9 @@ # Change Log -## Unreleased +## 24.1.0 ### Highlights - - This release introduces the new 2024 stable style (#4106), stabilizing the following changes: @@ -44,8 +42,6 @@ year's stable style. ### Stable style - - Several bug fixes were made in features that are moved to the stable style in this release: @@ -59,14 +55,12 @@ release: ### Preview style - - - Add `--unstable` style, covering preview features that have known problems that would block them from going into the stable style. Also add the `--enable-unstable-feature` flag; for example, use `--enable-unstable-feature hug_parens_with_braces_and_square_brackets` to apply this - preview style throughout 2024, even if a later Black release downgrades the feature to - unstable (#4096) + preview feature throughout 2024, even if a later Black release downgrades the feature + to unstable (#4096) - Format module docstrings the same as class and function docstrings (#4095) - Fix crash when using a walrus in a dictionary (#4155) - Fix unnecessary parentheses when wrapping long dicts (#4135) @@ -74,48 +68,18 @@ release: ### Configuration - - -- Print warning when toml config contains an invalid key (#4165) -- Fix symlink handling, properly catch and ignore symlinks that point outside of root - (#4161) +- Print warning when configuration in `pyproject.toml` contains an invalid key (#4165) +- Fix symlink handling, properly ignoring symlinks that point outside of root (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) - Remove the long-deprecated `--experimental-string-processing` flag. This feature can currently be enabled with `--preview --enable-unstable-feature string_processing`. (#4096) -### Packaging - - - -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - ### Integrations - - - Revert the change to run Black's pre-commit integration only on specific git hooks (#3940) for better compatibility with older versions of pre-commit (#4137) -### Documentation - - - ## 23.12.1 ### Packaging diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 3b895193941..259c1c1eaf3 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.0 hooks: - id: black-jupyter # 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 a42e093155b..562fd7d5905 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -266,8 +266,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.12.1 (compiled: yes) -$ black --required-version 23.12.1 -c "format = 'this'" +black, 24.1.0 (compiled: yes) +$ black --required-version 24.1.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -363,7 +363,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.12.1 +black, 24.1.0 ``` #### `--config` diff --git a/scripts/release.py b/scripts/release.py index d588429c2d3..c5336506396 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -169,7 +169,9 @@ def get_next_version(self) -> str: calver_parts = base_calver.split(".") base_calver = f"{calver_parts[0]}.{int(calver_parts[1])}" # Remove leading 0 git_tags = get_git_tags() - same_month_releases = [t for t in git_tags if t.startswith(base_calver)] + same_month_releases = [ + t for t in git_tags if t.startswith(base_calver) and "a" not in t + ] if len(same_month_releases) < 1: return f"{base_calver}.0" same_month_version = same_month_releases[-1].split(".", 2)[-1] From 659c29a41c7c686687aef21f57b95bcfa236b03b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 Jan 2024 21:12:38 -0800 Subject: [PATCH 695/700] New changelog --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index ff921de69eb..9a9be4bbeae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 24.1.0 ### Highlights From ed770ba4dd50c419148a0fca2b43937a7447e1f9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 26 Jan 2024 11:54:49 -0800 Subject: [PATCH 696/700] Fix cache file length (#4176) - Ensure total file length stays under 96 - Hash the path only if it's too long - Proceed normally (with a warning) if the cache can't be read Fixes #4172 --- CHANGES.md | 3 +++ src/black/cache.py | 9 ++++++++- src/black/mode.py | 21 +++++++++++++++++---- tests/test_black.py | 25 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9a9be4bbeae..e4240eacfca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,9 @@ +- Shorten the length of the name of the cache file to fix crashes on file systems that + do not support long paths (#4176) + ### Packaging diff --git a/src/black/cache.py b/src/black/cache.py index cfdbc21e92a..35bddb573d2 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -13,6 +13,7 @@ from _black_version import version as __version__ from black.mode import Mode +from black.output import err if sys.version_info >= (3, 11): from typing import Self @@ -64,7 +65,13 @@ def read(cls, mode: Mode) -> Self: resolve the issue. """ cache_file = get_cache_file(mode) - if not cache_file.exists(): + try: + exists = cache_file.exists() + except OSError as e: + # Likely file too long; see #4172 and #4174 + err(f"Unable to read cache file {cache_file} due to {e}") + return cls(mode, cache_file) + if not exists: return cls(mode, cache_file) with cache_file.open("rb") as fobj: diff --git a/src/black/mode.py b/src/black/mode.py index 68919fb4901..128d2b9f108 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -192,6 +192,9 @@ class Deprecated(UserWarning): """Visible deprecation warning.""" +_MAX_CACHE_KEY_PART_LENGTH: Final = 32 + + @dataclass class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) @@ -228,6 +231,19 @@ def get_cache_key(self) -> str: ) else: version_str = "-" + if len(version_str) > _MAX_CACHE_KEY_PART_LENGTH: + version_str = sha256(version_str.encode()).hexdigest()[ + :_MAX_CACHE_KEY_PART_LENGTH + ] + features_and_magics = ( + ",".join(sorted(f.name for f in self.enabled_features)) + + "@" + + ",".join(sorted(self.python_cell_magics)) + ) + if len(features_and_magics) > _MAX_CACHE_KEY_PART_LENGTH: + features_and_magics = sha256(features_and_magics.encode()).hexdigest()[ + :_MAX_CACHE_KEY_PART_LENGTH + ] parts = [ version_str, str(self.line_length), @@ -236,10 +252,7 @@ def get_cache_key(self) -> str: str(int(self.is_ipynb)), str(int(self.skip_source_first_line)), str(int(self.magic_trailing_comma)), - sha256( - (",".join(sorted(f.name for f in self.enabled_features))).encode() - ).hexdigest(), str(int(self.preview)), - sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(), + features_and_magics, ] return ".".join(parts) diff --git a/tests/test_black.py b/tests/test_black.py index 6dbe25a90b6..123ea0bb88a 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -44,6 +44,7 @@ from black import re_compile_maybe_verbose as compile_pattern from black.cache import FileData, get_cache_dir, get_cache_file from black.debug import DebugVisitor +from black.mode import Mode, Preview from black.output import color_diff, diff from black.report import Report @@ -2065,6 +2066,30 @@ def test_get_cache_dir( monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2)) assert get_cache_dir().parent == workspace2 + def test_cache_file_length(self) -> None: + cases = [ + DEFAULT_MODE, + # all of the target versions + Mode(target_versions=set(TargetVersion)), + # all of the features + Mode(enabled_features=set(Preview)), + # all of the magics + Mode(python_cell_magics={f"magic{i}" for i in range(500)}), + # all of the things + Mode( + target_versions=set(TargetVersion), + enabled_features=set(Preview), + python_cell_magics={f"magic{i}" for i in range(500)}, + ), + ] + for case in cases: + cache_file = get_cache_file(case) + # Some common file systems enforce a maximum path length + # of 143 (issue #4174). We can't do anything if the directory + # path is too long, but ensure the name of the cache file itself + # doesn't get too crazy. + assert len(cache_file.name) <= 96 + def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: From 1607e9ab20ad550cf940482d0d361ca31fc03189 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 27 Jan 2024 12:34:02 -0800 Subject: [PATCH 697/700] Fix missing space in option description (#4182) --- 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 ebc7ac8eda5..8ab5b47f974 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -279,7 +279,7 @@ def validate_regex( is_flag=True, help=( "Format all input files like Jupyter Notebooks regardless of file extension." - "This is useful when piping source on standard input." + " This is useful when piping source on standard input." ), ) @click.option( From 8bf04549ffd276a1bad6eb110e66e6557ee630d9 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:55:22 -0600 Subject: [PATCH 698/700] Consistently add trailing comma on typed parameters (#4164) Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 ++ docs/the_black_code_style/future_style.md | 2 ++ src/black/files.py | 2 +- src/black/linegen.py | 10 +++++++- src/black/mode.py | 1 + src/blib2to3/pgen2/parse.py | 2 +- .../data/cases/typed_params_trailing_comma.py | 24 +++++++++++++++++++ 7 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 tests/data/cases/typed_params_trailing_comma.py diff --git a/CHANGES.md b/CHANGES.md index e4240eacfca..6278aed77d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ +- Consistently add trailing comma on typed parameters (#4164) + ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 1cdd25fdb7c..d5faae36911 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -26,6 +26,8 @@ Currently, the following features are included in the preview style: brackets ([see below](labels/hug-parens)) - `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no longer normalized +- `typed_params_trailing_comma`: consistently add trailing commas to typed function + parameters (labels/unstable-features)= diff --git a/src/black/files.py b/src/black/files.py index 65951efdbe8..1eb8745572b 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -131,7 +131,7 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: def infer_target_version( - pyproject_toml: Dict[str, Any] + pyproject_toml: Dict[str, Any], ) -> Optional[List[TargetVersion]]: """Infer Black's target version from the project metadata in pyproject.toml. diff --git a/src/black/linegen.py b/src/black/linegen.py index a276805f2fe..c74ff9c0b4b 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -48,6 +48,7 @@ is_one_sequence_between, is_one_tuple, is_parent_function_or_class, + is_part_of_annotation, is_rpar_token, is_stub_body, is_stub_suite, @@ -1041,7 +1042,14 @@ def bracket_split_build_line( no_commas = ( original.is_def and opening_bracket.value == "(" - and not any(leaf.type == token.COMMA for leaf in leaves) + and not any( + leaf.type == token.COMMA + and ( + Preview.typed_params_trailing_comma not in original.mode + or not is_part_of_annotation(leaf) + ) + for leaf in leaves + ) # In particular, don't add one within a parenthesized return annotation. # Unfortunately the indicator we're in a return annotation (RARROW) may # be defined directly in the parent node, the parent of the parent ... diff --git a/src/black/mode.py b/src/black/mode.py index 128d2b9f108..22352e7c6a8 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -176,6 +176,7 @@ class Preview(Enum): no_normalize_fmt_skip_whitespace = auto() wrap_long_dict_values_in_parens = auto() multiline_string_handling = auto() + typed_params_trailing_comma = auto() UNSTABLE_FEATURES: Set[Preview] = { diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index ad51a3dad08..ad1d795b51a 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -50,7 +50,7 @@ def lam_sub(grammar: Grammar, node: RawNode) -> NL: def stack_copy( - stack: List[Tuple[DFAS, int, RawNode]] + stack: List[Tuple[DFAS, int, RawNode]], ) -> List[Tuple[DFAS, int, RawNode]]: """Nodeless stack copy.""" return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack] diff --git a/tests/data/cases/typed_params_trailing_comma.py b/tests/data/cases/typed_params_trailing_comma.py new file mode 100644 index 00000000000..a53b908b18b --- /dev/null +++ b/tests/data/cases/typed_params_trailing_comma.py @@ -0,0 +1,24 @@ +# flags: --preview +def long_function_name_goes_here( + x: Callable[List[int]] +) -> Union[List[int], float, str, bytes, Tuple[int]]: + pass + + +def long_function_name_goes_here( + x: Callable[[str, Any], int] +) -> Union[List[int], float, str, bytes, Tuple[int]]: + pass + + +# output +def long_function_name_goes_here( + x: Callable[List[int]], +) -> Union[List[int], float, str, bytes, Tuple[int]]: + pass + + +def long_function_name_goes_here( + x: Callable[[str, Any], int], +) -> Union[List[int], float, str, bytes, Tuple[int]]: + pass From 79fc1158a98281dac798feb14b8fddb4051e4a42 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 27 Jan 2024 23:24:36 -0500 Subject: [PATCH 699/700] chore: ignore node_modules (produced by a pre-commit check) (#4184) Signed-off-by: Henry Schreiner --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4a4f1b738ad..6e23797d110 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ src/_black_version.py .hypothesis/ venv/ .ipynb_checkpoints/ +node_modules/ From e026c93888f91a47a9c9f4e029f3eb07d96375e6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 27 Jan 2024 20:51:32 -0800 Subject: [PATCH 700/700] Prepare release 24.1.1 (#4186) --- CHANGES.md | 44 ++------------------- docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- 3 files changed, 8 insertions(+), 46 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6278aed77d8..a794f421694 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,57 +1,19 @@ # Change Log -## Unreleased +## 24.1.1 -### Highlights - - - -### Stable style - - +Bugfix release to fix a bug that made Black unusable on certain file systems with strict +limits on path length. ### Preview style - - - Consistently add trailing comma on typed parameters (#4164) ### Configuration - - - Shorten the length of the name of the cache file to fix crashes on file systems that do not support long paths (#4176) -### Packaging - - - -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - -### Integrations - - - -### Documentation - - - ## 24.1.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 259c1c1eaf3..92279707d84 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.0 + rev: 24.1.1 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.0 + rev: 24.1.1 hooks: - id: black-jupyter # 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 562fd7d5905..dc9d9a64c68 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -266,8 +266,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 24.1.0 (compiled: yes) -$ black --required-version 24.1.0 -c "format = 'this'" +black, 24.1.1 (compiled: yes) +$ black --required-version 24.1.1 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -363,7 +363,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 24.1.0 +black, 24.1.1 ``` #### `--config`