From 2471b9256d9d9dfea1124d20072201693b9b0865 Mon Sep 17 00:00:00 2001 From: Lihu Ben-Ezri-Ravin Date: Wed, 24 Jun 2020 05:09:07 -0400 Subject: [PATCH 001/680] Find project root correctly (#1518) Ensure root dir is a common parent of all inputs Fixes #1493 --- src/black/__init__.py | 23 ++++++++++++++++------- tests/test_black.py | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 2b2d3d88c73..d4c6e62bdbf 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5825,8 +5825,8 @@ def gen_python_files( def find_project_root(srcs: Iterable[str]) -> Path: """Return a directory containing .git, .hg, or pyproject.toml. - That directory can be one of the directories passed in `srcs` or their - common parent. + That directory will be a common parent of all files and directories + passed in `srcs`. 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. @@ -5834,11 +5834,20 @@ def find_project_root(srcs: Iterable[str]) -> Path: if not srcs: return Path("/").resolve() - common_base = min(Path(src).resolve() for src in srcs) - if common_base.is_dir(): - # Append a fake file so `parents` below returns `common_base_dir`, too. - common_base /= "fake-file" - for directory in common_base.parents: + path_srcs = [Path(src).resolve() for src in srcs] + + # A list of lists of parents for each 'src'. 'src' is included as a + # "parent" of itself if it is a directory + src_parents = [ + list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs + ] + + common_base = max( + set.intersection(*(set(parents) for parents in src_parents)), + key=lambda path: path.parts, + ) + + for directory in (common_base, *common_base.parents): if (directory / ".git").exists(): return directory diff --git a/tests/test_black.py b/tests/test_black.py index 88839d86c5a..3ed5daa4b49 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1801,6 +1801,28 @@ def __init__(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + def test_find_project_root(self) -> None: + with TemporaryDirectory() as workspace: + root = Path(workspace) + test_dir = root / "test" + test_dir.mkdir() + + src_dir = root / "src" + src_dir.mkdir() + + root_pyproject = root / "pyproject.toml" + root_pyproject.touch() + src_pyproject = src_dir / "pyproject.toml" + src_pyproject.touch() + src_python = src_dir / "foo.py" + src_python.touch() + + self.assertEqual( + black.find_project_root((src_dir, test_dir)), root.resolve() + ) + self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) + self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) + class BlackDTestCase(AioHTTPTestCase): async def get_application(self) -> web.Application: From 81a093e82a860c4b7e559532a847ccd967e6e456 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 1 Jul 2020 08:15:24 -0700 Subject: [PATCH 002/680] Add pip install from GitHub command to README.md (#1529) * Add pip install from GitHub command to README.md * Make it prettier ... --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index beed8ba4943..4b65fb794ee 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ _Contents:_ **[Installation and usage](#installation-and-usage)** | _Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to run but you can reformat Python 2 code with it, too. +#### Install from GitHub + +If you can't wait for the latest _hotness_ and want to install from GitHub, use: + +`pip install git+git://github.com/psf/black` + ### Usage To get started right away with sensible defaults: From 3209bf7ffa072fe6bc6fa700cec484bcdbd1d57c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 6 Jul 2020 16:06:34 +0200 Subject: [PATCH 003/680] Mozilla uses black too (#1531) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b65fb794ee..5ac76cbf786 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, every Datadog Agent Integration, Home Assistant. -The following organizations use _Black_: Facebook, Dropbox. +The following organizations use _Black_: Facebook, Dropbox, Mozilla. Are we missing anyone? Let us know. From 11f130b7deb74f424ebf57ef551953c9bc08393f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 6 Jul 2020 12:45:13 -0700 Subject: [PATCH 004/680] add Quora to orgs that use Black (#1532) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ac76cbf786..fc2d4decf95 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, every Datadog Agent Integration, Home Assistant. -The following organizations use _Black_: Facebook, Dropbox, Mozilla. +The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora. Are we missing anyone? Let us know. From cc2facaac69a8ffa4486e6fe498842debb17ce12 Mon Sep 17 00:00:00 2001 From: Olexiy Date: Wed, 8 Jul 2020 18:51:18 +0300 Subject: [PATCH 005/680] ISSUE 1533: Fix --config argument description (#1534) Change --config argument description to "Read configuration from FILE." The "--config FILE Read configuration from FILE path" --- README.md | 2 +- src/black/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc2d4decf95..33c170bd3c8 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Options: --exclude=. --version Show the version and exit. - --config FILE Read configuration from PATH. + --config FILE Read configuration from FILE path. -h, --help Show this message and exit. ``` diff --git a/src/black/__init__.py b/src/black/__init__.py index d4c6e62bdbf..3d0a2d69069 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -471,7 +471,7 @@ def target_version_option_callback( ), is_eager=True, callback=read_pyproject_toml, - help="Read configuration from PATH.", + help="Read configuration from FILE path.", ) @click.pass_context def main( From 8d036ceb3f0a0e0b52276e8d4afe840d1fcad11b Mon Sep 17 00:00:00 2001 From: jtpavlock Date: Mon, 13 Jul 2020 15:27:05 -0700 Subject: [PATCH 006/680] Spelling fix in CONTRIBUTING.md (#1547) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 525cb9c183d..0687aaeee52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ $ black-primer [-k -w /tmp/black_test_repos] ## black-primer `black-primer` is used by CI to pull down well-known _Black_ formatted projects and see -if we get soure code changes. It will error on formatting changes or errors. Please run +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/master/src/black_primer/primer.json) From 98ac69f04ccaf3328b05e65d19a24f205825be4f Mon Sep 17 00:00:00 2001 From: dhaug-op <56020126+dhaug-op@users.noreply.github.com> Date: Wed, 15 Jul 2020 17:06:30 +0200 Subject: [PATCH 007/680] Ensure path for finding root is absolute (#1550) As Path.resolve() is buggy on windows (see https://bugs.python.org/issue38671) an absolute path is ensured by prepending the Path.cwd() --- 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 3d0a2d69069..930f2cb0ae5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5834,7 +5834,7 @@ def find_project_root(srcs: Iterable[str]) -> Path: if not srcs: return Path("/").resolve() - path_srcs = [Path(src).resolve() for src in srcs] + path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] # A list of lists of parents for each 'src'. 'src' is included as a # "parent" of itself if it is a directory From 5010bb42798d6a5c7a5e35a54baad96df6f6814d Mon Sep 17 00:00:00 2001 From: Steven Maude Date: Wed, 15 Jul 2020 19:36:14 +0100 Subject: [PATCH 008/680] Update curl command to use stable branch (#1543) --- docs/editor_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index 00241f23335..eb83a1a4b43 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -146,7 +146,7 @@ or you can copy the plugin from ``` mkdir -p ~/.vim/pack/python/start/black/plugin -curl https://raw.githubusercontent.com/psf/black/master/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim +curl https://raw.githubusercontent.com/psf/black/stable/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim ``` Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or From 2955bdc6767d942cd489c73bd3bf600303d524c5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 16 Jul 2020 00:53:48 +0300 Subject: [PATCH 009/680] pre-commit: show diff on failure on CI (#1552) * pre-commit: --show-diff-on-failure * pre-commit: --show-diff-on-failure --- .github/workflows/lint.yml | 2 +- .travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb8c534c9ac..fa7286eec1f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,4 +24,4 @@ jobs: python -m pip install -e '.[d]' - name: Lint - run: pre-commit run --all-files + run: pre-commit run --all-files --show-diff-on-failure diff --git a/.travis.yml b/.travis.yml index b2b127cfeb0..86cf24df51b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: - name: "lint" python: 3.7 env: - - TEST_CMD="pre-commit run --all-files" + - TEST_CMD="pre-commit run --all-files --show-diff-on-failure" - name: "3.6" python: 3.6 - name: "3.7" From 2c5041cfa0e5645275504c070ef7d0ca5609752f Mon Sep 17 00:00:00 2001 From: Vinicius Gubiani Ferreira Date: Thu, 16 Jul 2020 01:51:39 -0300 Subject: [PATCH 010/680] docs: Improve pre-commit use (#1551) Stable tag wasn't available and crashed when attempting to set initial pre-commit. Also the python version needs to be installed so it would be better to use the generic "python3" command. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33c170bd3c8..073150834d3 100644 --- a/README.md +++ b/README.md @@ -370,10 +370,10 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: stable + rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags hooks: - id: black - language_version: python3.6 + language_version: python3 # Should be a command that runs python3.6+ ``` Then run `pre-commit install` and you're ready to go. From 537ea8df35b1004bdb228b483907fb5dd92e5257 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Wed, 22 Jul 2020 02:29:38 +0200 Subject: [PATCH 011/680] Update to accomodate isort 5 release changes. (#1559) Isort 5 introduced profiles and ensure_newline_before_comments options. Either needs to be added to work correctly with black. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/compatible_configs.md | 15 ++++++++++++++- docs/the_black_code_style.md | 13 +++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index aa7cd96f165..723fc889c00 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -23,6 +23,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -62,7 +63,15 @@ The option `force_grid_wrap = 0` is just to tell isort to only wrap imports that the `line_length` limit. Finally, isort should be told to wrap imports when they surpass _Black_'s default limit -of 88 characters via `line_length = 88`. +of 88 characters via `line_length = 88` as well as +`ensure_newline_before_comments = True` to ensure spacing import sections with comments +works the same as with _Black_. + +**Please note** `ensure_newline_before_comments = True` only works since isort >= 5 but +does not break older versions so you can keep it if you are running previous versions. +If only isort >= 5 is used you can add `profile = black` instead of all the options +since [profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/) +are available and do the configuring for you. ### Formats @@ -75,6 +84,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -89,6 +99,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -103,6 +114,7 @@ multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true +ensure_newline_before_comments = true line_length = 88 ``` @@ -117,6 +129,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 21f217d388e..09d58307a05 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -153,13 +153,14 @@ the following configuration.
A compatible `.isort.cfg` -``` +```cfg [settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 88 ``` The equivalent command line is: From b59a5246577346e6da2cc2802015f08524abf545 Mon Sep 17 00:00:00 2001 From: Kaligule Date: Sat, 1 Aug 2020 23:21:55 +0200 Subject: [PATCH 012/680] fix spelling (#1567) Co-authored-by: Hugo van Kemenade --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 073150834d3..d4b239b7358 100644 --- a/README.md +++ b/README.md @@ -354,8 +354,8 @@ rolling. ## black-primer -`black-primer` is a tool built for CI (and huumans) to have _Black_ `--check` a number -of (configured in `primer.json`) Git accessible projects in parallel. +`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of +(configured in `primer.json`) Git accessible projects in parallel. [black_primer](https://github.com/psf/black/blob/master/docs/black_primer.md) has more information regarding its usage and configuration. From f825e7ef28c65689e06453d0c3a00310b90cdfdb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 12 Aug 2020 19:12:21 -0700 Subject: [PATCH 013/680] Remove slow assertion (#1592) Partial fix for #1581 This assertion produces behavior quadratic in the number of leaves in a line, which is making Black extremely slow on files with very long expressions. On my benchmark file this change makes Black 10x faster. --- src/black/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 930f2cb0ae5..7ce2ac896b3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -4594,8 +4594,6 @@ def append_leaves(new_line: Line, old_line: Line, leaves: List[Leaf]) -> None: set(@leaves) is a subset of set(@old_line.leaves). """ for old_leaf in leaves: - assert old_leaf in old_line.leaves - new_leaf = Leaf(old_leaf.type, old_leaf.value) replace_child(old_leaf, new_leaf) new_line.append(new_leaf) From 149b38d67430f0c580a05c821db767592e7d55e2 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Wed, 12 Aug 2020 19:28:01 -0700 Subject: [PATCH 014/680] Add the direnv base directory to the default excludes (#1564) Co-authored-by: Chris Rose --- README.md | 1 + src/black/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4b239b7358..aead10cf585 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,7 @@ Multiple contributions by: - [Christian Clauss](mailto:cclauss@bluewin.ch) - [Christian Heimes](mailto:christian@python.org) - [Chuck Wooters](mailto:chuck.wooters@microsoft.com) +- [Chris Rose](mailto:offline@offby1.net) - Codey Oxley - [Cong](mailto:congusbongus@gmail.com) - [Cooper Ry Lees](mailto:me@cooperlees.com) diff --git a/src/black/__init__.py b/src/black/__init__.py index 7ce2ac896b3..df38fd01a9b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -65,7 +65,7 @@ import colorama # noqa: F401 DEFAULT_LINE_LENGTH = 88 -DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 +DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 DEFAULT_INCLUDES = r"\.pyi?$" CACHE_DIR = Path(user_cache_dir("black", version=__version__)) From 97c11f22aaf3eacad42d4f78309ffc1f6965e955 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 12 Aug 2020 23:07:19 -0400 Subject: [PATCH 015/680] Make --exclude only apply to recursively found files (#1591) Ever since --force-exclude was added, --exclude started to touch files that were given to Black through the CLI too. This is not documented behaviour and neither expected as --exclude and --force-exclude now behave the same! Before this commit, get_sources() when encountering a file that was passed explicitly through the CLI would pass a single Path object list to gen_python_files(). This causes bad behaviour since that function doesn't treat the exclude and force_exclude regexes differently. Which is fine for recursively found files, but *not* for files given through the CLI. Now when get_sources() iterates through srcs and encounters a file, it checks if the force_exclude regex matches, if not, then the file will be added to the computed sources set. A new function had to be created since before you can do regex matching, the path must be normalized. The full process of normalizing the path is somewhat long as there is special error handling. I didn't want to duplicate this logic in get_sources() and gen_python_files() so that's why there is a new helper function. --- src/black/__init__.py | 99 ++++++++++++++++++++++++++++--------------- tests/test_black.py | 63 ++++++++++++++++++++------- 2 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index df38fd01a9b..803c7a1c633 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -583,9 +583,7 @@ def get_sources( root = find_project_root(src) sources: Set[Path] = set() path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) - exclude_regexes = [exclude_regex] - if force_exclude_regex is not None: - exclude_regexes.append(force_exclude_regex) + gitignore = get_gitignore(root) for s in src: p = Path(s) @@ -595,19 +593,30 @@ def get_sources( p.iterdir(), root, include_regex, - exclude_regexes, + exclude_regex, + force_exclude_regex, report, - get_gitignore(root), + gitignore, ) ) elif s == "-": sources.add(p) elif p.is_file(): - sources.update( - gen_python_files( - [p], root, None, exclude_regexes, report, get_gitignore(root) - ) - ) + normalized_path = normalize_path_maybe_ignore(p, root, report) + if normalized_path is None: + continue + + normalized_path = "/" + normalized_path + # Hard-exclude any files that matches the `--force-exclude` regex. + if force_exclude_regex: + force_exclude_match = force_exclude_regex.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") + continue + + sources.add(p) else: err(f"invalid path: {s}") return sources @@ -5757,16 +5766,40 @@ def get_gitignore(root: Path) -> PathSpec: return PathSpec.from_lines("gitwildmatch", lines) +def normalize_path_maybe_ignore( + path: Path, root: Path, report: "Report" +) -> Optional[str]: + """Normalize `path`. May return `None` if `path` was ignored. + + `report` is where "path ignored" output goes. + """ + try: + normalized_path = path.resolve().relative_to(root).as_posix() + except OSError as e: + 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}") + return None + + raise + + return normalized_path + + def gen_python_files( paths: Iterable[Path], root: Path, include: Optional[Pattern[str]], - exclude_regexes: Iterable[Pattern[str]], + exclude: Pattern[str], + force_exclude: Optional[Pattern[str]], report: "Report", gitignore: PathSpec, ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the - `exclude` regex, but are included by the `include` regex. + `exclude_regex` or `force_exclude` regexes, but are included by the `include` regex. Symbolic links pointing outside of the `root` directory are ignored. @@ -5774,43 +5807,41 @@ def gen_python_files( """ assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: - # Then ignore with `exclude` option. - try: - normalized_path = child.resolve().relative_to(root).as_posix() - except OSError as e: - report.path_ignored(child, f"cannot be read because {e}") + normalized_path = normalize_path_maybe_ignore(child, root, report) + if normalized_path is None: continue - except ValueError: - if child.is_symlink(): - report.path_ignored( - child, f"is a symbolic link that points outside {root}" - ) - continue - - raise # First ignore files matching .gitignore if gitignore.match_file(normalized_path): report.path_ignored(child, "matches the .gitignore file content") continue + # Then ignore with `--exclude` and `--force-exclude` options. normalized_path = "/" + normalized_path if child.is_dir(): normalized_path += "/" - is_excluded = False - for exclude in exclude_regexes: - exclude_match = exclude.search(normalized_path) if exclude else None - if exclude_match and exclude_match.group(0): - report.path_ignored(child, "matches the --exclude regular expression") - is_excluded = True - break - if is_excluded: + exclude_match = exclude.search(normalized_path) if exclude else None + if exclude_match and exclude_match.group(0): + report.path_ignored(child, "matches the --exclude regular expression") + continue + + force_exclude_match = ( + force_exclude.search(normalized_path) if force_exclude else None + ) + if force_exclude_match and force_exclude_match.group(0): + report.path_ignored(child, "matches the --force-exclude regular expression") continue if child.is_dir(): yield from gen_python_files( - child.iterdir(), root, include, exclude_regexes, report, gitignore + child.iterdir(), + root, + include, + exclude, + force_exclude, + report, + gitignore, ) elif child.is_file(): diff --git a/tests/test_black.py b/tests/test_black.py index 3ed5daa4b49..f4055581f86 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -110,6 +110,20 @@ def skip_if_exception(e: str) -> Iterator[None]: raise +class FakeContext(click.Context): + """A fake click Context for when calling functions that need it.""" + + def __init__(self) -> None: + self.default_map: Dict[str, Any] = {} + + +class FakeParameter(click.Parameter): + """A fake click Parameter for when calling functions that need it.""" + + def __init__(self) -> None: + pass + + class BlackRunner(CliRunner): """Modify CliRunner so that stderr is not merged with stdout. @@ -1551,7 +1565,32 @@ def test_include_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, [exclude], report, gitignore + path.iterdir(), this_abs, include, exclude, None, report, gitignore + ) + ) + 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=include, + exclude=exclude, + force_exclude=None, + report=report, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1572,7 +1611,7 @@ def test_gitignore_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, [exclude], report, gitignore + path.iterdir(), this_abs, include, exclude, None, report, gitignore ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1600,7 +1639,8 @@ def test_empty_include(self) -> None: path.iterdir(), this_abs, empty, - [re.compile(black.DEFAULT_EXCLUDES)], + re.compile(black.DEFAULT_EXCLUDES), + None, report, gitignore, ) @@ -1627,7 +1667,8 @@ def test_empty_exclude(self) -> None: path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - [empty], + empty, + None, report, gitignore, ) @@ -1684,7 +1725,7 @@ def test_symlink_out_of_root_directory(self) -> None: try: list( black.gen_python_files( - path.iterdir(), root, include, exclude, report, gitignore + path.iterdir(), root, include, exclude, None, report, gitignore ) ) except ValueError as ve: @@ -1698,7 +1739,7 @@ def test_symlink_out_of_root_directory(self) -> None: with self.assertRaises(ValueError): list( black.gen_python_files( - path.iterdir(), root, include, exclude, report, gitignore + path.iterdir(), root, include, exclude, None, report, gitignore ) ) path.iterdir.assert_called() @@ -1777,16 +1818,6 @@ def test_parse_pyproject_toml(self) -> None: def test_read_pyproject_toml(self) -> None: test_toml_file = THIS_DIR / "test.toml" - - # Fake a click context and parameter so mypy stays happy - class FakeContext(click.Context): - def __init__(self) -> None: - self.default_map: Dict[str, Any] = {} - - class FakeParameter(click.Parameter): - def __init__(self) -> None: - pass - fake_ctx = FakeContext() black.read_pyproject_toml( fake_ctx, FakeParameter(), str(test_toml_file), From 8842c5ffa888aab2e4961fc54db45e7f3ca32ad9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 13 Aug 2020 11:14:34 -0700 Subject: [PATCH 016/680] in verbose mode, print stack trace (#1594) Make Black failures easier to debug --- 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 803c7a1c633..349da1e71e2 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -664,6 +664,8 @@ def reformat_one( write_cache(cache, [src], mode) report.done(src, changed) except Exception as exc: + if report.verbose: + traceback.print_exc() report.failed(src, str(exc)) @@ -3327,7 +3329,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: new_line.append(string_leaf) append_leaves( - new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :], + new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] ) LL[rpar_idx].remove() From 5e1f620af7ff5d2bb4205e398849e5395999a0cb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 13 Aug 2020 16:40:45 -0700 Subject: [PATCH 017/680] fix some docstring crashes (#1593) Allow removing some trailing whitespace --- src/black/__init__.py | 2 +- tests/data/docstring.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 349da1e71e2..36c03946b55 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6063,7 +6063,7 @@ def _stringify_ast( and field == "value" and isinstance(value, str) ): - normalized = re.sub(r" *\n[ \t]+", "\n ", value).strip() + normalized = re.sub(r" *\n[ \t]*", "\n", value).strip() else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" diff --git a/tests/data/docstring.py b/tests/data/docstring.py index f5adeb7bb7b..fcb8eb12a78 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -46,7 +46,7 @@ def zort(): def poit(): """ - Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet. Consectetur adipiscing elit: - sed do eiusmod tempor incididunt ut labore @@ -58,6 +58,14 @@ def poit(): pass +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + def over_indent(): """ This has a shallow indent @@ -136,6 +144,14 @@ def poit(): pass +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + def over_indent(): """ This has a shallow indent From d1ad8730e36819f787c720d1917de22b59806a75 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Fri, 14 Aug 2020 03:20:46 +0100 Subject: [PATCH 018/680] don't strip brackets before lsqb (#1575) (#1590) if the string contains a PERCENT, it's not safe to remove brackets that follow and operator with the same or higher precedence than PERCENT --- src/black/__init__.py | 45 +++++++++++++++++++++++++++++--- tests/data/percent_precedence.py | 39 +++++++++++++++++++++++++++ tests/test_black.py | 8 ++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/data/percent_precedence.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 36c03946b55..b660e4788b4 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3249,7 +3249,9 @@ class StringParenStripper(StringTransformer): Requirements: The line contains a string which is surrounded by parentheses and: - The target string is NOT the only argument to a function call). - - The RPAR is NOT followed by an attribute access (i.e. a dot). + - If the target string contains a PERCENT, the brackets are not + preceeded or followed by an operator with higher precedence than + PERCENT. Transformations: The parentheses mentioned in the 'Requirements' section are stripped. @@ -3292,14 +3294,51 @@ def do_match(self, line: Line) -> TMatchResult: string_parser = StringParser() next_idx = string_parser.parse(LL, string_idx) + # if the leaves in the parsed string include a PERCENT, we need to + # make sure the initial LPAR is NOT preceded by an operator with + # higher or equal precedence to PERCENT + if ( + is_valid_index(idx - 2) + and token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} + and ( + ( + LL[idx - 2].type + in { + token.STAR, + token.AT, + token.SLASH, + token.DOUBLESLASH, + token.PERCENT, + token.TILDE, + token.DOUBLESTAR, + token.AWAIT, + token.LSQB, + token.LPAR, + } + ) + or ( + # only unary PLUS/MINUS + not is_valid_index(idx - 3) + and (LL[idx - 2].type in {token.PLUS, token.MINUS}) + ) + ) + ): + continue + # Should be followed by a non-empty RPAR... if ( is_valid_index(next_idx) and LL[next_idx].type == token.RPAR and not is_empty_rpar(LL[next_idx]) ): - # That RPAR should NOT be followed by a '.' symbol. - if is_valid_index(next_idx + 1) and LL[next_idx + 1].type == token.DOT: + # That RPAR should NOT be followed by anything with higher + # precedence than PERCENT + if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { + token.DOUBLESTAR, + token.LSQB, + token.LPAR, + token.DOT, + }: continue return Ok(string_idx) diff --git a/tests/data/percent_precedence.py b/tests/data/percent_precedence.py new file mode 100644 index 00000000000..44d30f16e03 --- /dev/null +++ b/tests/data/percent_precedence.py @@ -0,0 +1,39 @@ +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + ("" % a) +-("" % a) +b - ("" % a) +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) +# output +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + "" % a +-("" % a) +b - "" % a +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) diff --git a/tests/test_black.py b/tests/test_black.py index f4055581f86..3c766330a08 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -501,6 +501,14 @@ def test_slices(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, black.FileMode()) + @patch("black.dump_to_file", dump_to_stderr) + def test_percent_precedence(self) -> None: + source, expected = read_data("percent_precedence") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + @patch("black.dump_to_file", dump_to_stderr) def test_comments(self) -> None: source, expected = read_data("comments") From 820f38708fd41a1b992716b1f65c9b0656f589d0 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Fri, 14 Aug 2020 17:17:56 +0100 Subject: [PATCH 019/680] fix unary op detection (#1600) --- src/black/__init__.py | 20 ++++++++++---------- tests/data/percent_precedence.py | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index b660e4788b4..391233ed448 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3297,12 +3297,12 @@ def do_match(self, line: Line) -> TMatchResult: # if the leaves in the parsed string include a PERCENT, we need to # make sure the initial LPAR is NOT preceded by an operator with # higher or equal precedence to PERCENT - if ( - is_valid_index(idx - 2) - and token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} - and ( + if is_valid_index(idx - 2): + # mypy can't quite follow unless we name this + before_lpar = LL[idx - 2] + if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and ( ( - LL[idx - 2].type + before_lpar.type in { token.STAR, token.AT, @@ -3318,12 +3318,12 @@ def do_match(self, line: Line) -> TMatchResult: ) or ( # only unary PLUS/MINUS - not is_valid_index(idx - 3) - and (LL[idx - 2].type in {token.PLUS, token.MINUS}) + before_lpar.parent + and before_lpar.parent.type == syms.factor + and (before_lpar.type in {token.PLUS, token.MINUS}) ) - ) - ): - continue + ): + continue # Should be followed by a non-empty RPAR... if ( diff --git a/tests/data/percent_precedence.py b/tests/data/percent_precedence.py index 44d30f16e03..b895443fb46 100644 --- a/tests/data/percent_precedence.py +++ b/tests/data/percent_precedence.py @@ -12,6 +12,7 @@ b + ("" % a) -("" % a) b - ("" % a) +b + -("" % a) ~("" % a) 2 ** ("" % a) await ("" % a) @@ -32,6 +33,7 @@ b + "" % a -("" % a) b - "" % a +b + -("" % a) ~("" % a) 2 ** ("" % a) await ("" % a) From 80425ca440136002a64e24d6be7c3ba59cc20f9c Mon Sep 17 00:00:00 2001 From: Daanyaal Syed Date: Mon, 17 Aug 2020 14:55:38 -0400 Subject: [PATCH 020/680] Fix inline code style in README (#1608) Refer to `pyproject.toml` in HTML section of README with HTML code tags --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aead10cf585..37b93c143a2 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ the equivalent of r-strings in Python. Multiline strings are treated as verbose expressions by Black. Use `[ ]` to denote a significant space character.
-Example `pyproject.toml` +Example pyproject.toml ```toml [tool.black] From e1027e2bee45ce4830febf9e5e9b4d3f98ec5bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 18 Aug 2020 12:31:15 +0200 Subject: [PATCH 021/680] Update all dependencies to latest versions --- Pipfile | 2 +- Pipfile.lock | 624 +++++++++++++++++++++++---------------------------- 2 files changed, 284 insertions(+), 342 deletions(-) diff --git a/Pipfile b/Pipfile index 3afe3bb9efd..feefb857132 100644 --- a/Pipfile +++ b/Pipfile @@ -18,7 +18,7 @@ setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" wheel = ">=0.31.1" -black = {editable = true,extras = ["d"],path = "."} +black = {editable = true, extras = ["d"], path = "."} [packages] aiohttp = ">=3.3.2" diff --git a/Pipfile.lock b/Pipfile.lock index b0da4743793..b8dfd6b5422 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -42,17 +42,18 @@ }, "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], "index": "pypi", - "version": "==1.4.3" + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -60,6 +61,7 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "black": { @@ -78,11 +80,11 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], "index": "pypi", - "version": "==7.1.1" + "version": "==7.1.2" }, "dataclasses": { "hashes": [ @@ -90,36 +92,40 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "version": "==0.6" + "python_version <": "3.7", + "version": "==0.6", + "version >": "0.6" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "multidict": { "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "mypy-extensions": { "hashes": [ @@ -131,38 +137,38 @@ }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "index": "pypi", - "version": "==2020.2.20" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "index": "pypi", + "version": "==2020.7.14" }, "setuptools-scm": { "hashes": [ @@ -180,10 +186,10 @@ }, "toml": { "hashes": [ - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" @@ -216,34 +222,35 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" ], "index": "pypi", - "version": "==3.7.4.1" + "version": "==3.7.4.2" }, "yarl": { "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } }, "develop": { @@ -282,17 +289,18 @@ }, "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], "index": "pypi", - "version": "==1.4.3" + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -300,6 +308,7 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "babel": { @@ -327,51 +336,18 @@ }, "certifi": { "hashes": [ - "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", - "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" - ], - "version": "==2020.4.5.2" - }, - "cffi": { - "hashes": [ - "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", - "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", - "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", - "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", - "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", - "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", - "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", - "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", - "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", - "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", - "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", - "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", - "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", - "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", - "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", - "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", - "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", - "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", - "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", - "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", - "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", - "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", - "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", - "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", - "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", - "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", - "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", - "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" - ], - "version": "==1.14.0" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" }, "cfgv": { "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" + "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", + "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], "markers": "python_full_version >= '3.6.1'", - "version": "==3.1.0" + "version": "==3.2.0" }, "chardet": { "hashes": [ @@ -382,11 +358,19 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], "index": "pypi", - "version": "==7.1.1" + "version": "==7.1.2" + }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.3" }, "commonmark": { "hashes": [ @@ -397,71 +381,50 @@ }, "coverage": { "hashes": [ - "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0", - "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30", - "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b", - "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0", - "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823", - "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe", - "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037", - "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6", - "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31", - "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd", - "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892", - "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1", - "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78", - "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac", - "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006", - "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014", - "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2", - "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7", - "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8", - "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7", - "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9", - "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1", - "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307", - "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a", - "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435", - "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0", - "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5", - "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441", - "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732", - "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de", - "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1" - ], - "index": "pypi", - "version": "==5.0.4" - }, - "cryptography": { - "hashes": [ - "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", - "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", - "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", - "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", - "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", - "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", - "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", - "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", - "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", - "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", - "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", - "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", - "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", - "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", - "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", - "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", - "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", - "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", - "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.9.2" + "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", + "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", + "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", + "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", + "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", + "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", + "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", + "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", + "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", + "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", + "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", + "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", + "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", + "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", + "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", + "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", + "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", + "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", + "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", + "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", + "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", + "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", + "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", + "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", + "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", + "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", + "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", + "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", + "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", + "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", + "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", + "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", + "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", + "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + ], + "index": "pypi", + "version": "==5.2.1" }, "distlib": { "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "docutils": { "hashes": [ @@ -472,13 +435,6 @@ "index": "pypi", "version": "==0.15" }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, "filelock": { "hashes": [ "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", @@ -488,11 +444,11 @@ }, "flake8": { "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], "index": "pypi", - "version": "==3.7.9" + "version": "==3.8.3" }, "flake8-bugbear": { "hashes": [ @@ -512,18 +468,19 @@ }, "identify": { "hashes": [ - "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52", - "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef" + "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", + "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.19" + "version": "==1.4.28" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "imagesize": { "hashes": [ @@ -533,14 +490,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", @@ -551,11 +500,11 @@ }, "keyring": { "hashes": [ - "sha256:3401234209015144a5d75701e71cb47239e552b0882313e9f51e8976f9e27843", - "sha256:c53e0e5ccde3ad34284a40ce7976b5b3a3d6de70344c3f8ee44364cc340976ec" + "sha256:22df6abfed49912fc560806030051067fba9f0069cffa79da72899aeea4ccbd5", + "sha256:e7a17caf40c40b6bb8c4772224a487e4a63013560ed0c521065aeba7ecd42182" ], "markers": "python_version >= '3.6'", - "version": "==21.2.1" + "version": "==21.3.0" }, "markupsafe": { "hashes": [ @@ -605,45 +554,46 @@ }, "multidict": { "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "mypy": { "hashes": [ - "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", - "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", - "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", - "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", - "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", - "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", - "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", - "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", - "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", - "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", - "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", - "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", - "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", - "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" + "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", + "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", + "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", + "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", + "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", + "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", + "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", + "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", + "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", + "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", + "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", + "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", + "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", + "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" ], "index": "pypi", - "version": "==0.770" + "version": "==0.782" }, "mypy-extensions": { "hashes": [ @@ -669,11 +619,11 @@ }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "pkginfo": { "hashes": [ @@ -684,11 +634,11 @@ }, "pre-commit": { "hashes": [ - "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522", - "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1" + "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", + "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.6.0" }, "pycodestyle": { "hashes": [ @@ -698,14 +648,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -755,11 +697,11 @@ }, "readme-renderer": { "hashes": [ - "sha256:1b6d8dd1673a0b293766b4106af766b6eff3654605f9c4f239e65de6076bc222", - "sha256:e67d64242f0174a63c3b727801a2fff4c1f38ebe5d71d95ff7ece081945a6cd4" + "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", + "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" ], "index": "pypi", - "version": "==25.0" + "version": "==26.0" }, "recommonmark": { "hashes": [ @@ -771,38 +713,38 @@ }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "index": "pypi", - "version": "==2020.2.20" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "index": "pypi", + "version": "==2020.7.14" }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-toolbelt": { "hashes": [ @@ -811,13 +753,12 @@ ], "version": "==0.9.1" }, - "secretstorage": { + "rfc3986": { "hashes": [ - "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", - "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" + "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", + "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" ], - "markers": "sys_platform == 'linux'", - "version": "==3.1.2" + "version": "==1.4.0" }, "setuptools-scm": { "hashes": [ @@ -850,11 +791,11 @@ }, "sphinx": { "hashes": [ - "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66", - "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb" + "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", + "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0" ], "index": "pypi", - "version": "==2.4.4" + "version": "==3.2.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -906,29 +847,29 @@ }, "toml": { "hashes": [ - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" }, "tqdm": { "hashes": [ - "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c", - "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f" + "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", + "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.46.1" + "version": "==4.48.2" }, "twine": { "hashes": [ - "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124", - "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160" + "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", + "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.2.0" }, "typed-ast": { "hashes": [ @@ -958,28 +899,28 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" ], "index": "pypi", - "version": "==3.7.4.1" + "version": "==3.7.4.2" }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "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.25.9" + "version": "==1.25.10" }, "virtualenv": { "hashes": [ - "sha256:5102fbf1ec57e80671ef40ed98a84e980a71194cedf30c87c2b25c3a9e0b0107", - "sha256:ccfb8e1e05a1174f7bd4c163700277ba730496094fe1a58bea9d4ac140a207c8" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.23" + "version": "==20.0.31" }, "webencodings": { "hashes": [ @@ -990,33 +931,34 @@ }, "wheel": { "hashes": [ - "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96", - "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e" + "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2", + "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f" ], "index": "pypi", - "version": "==0.34.2" + "version": "==0.35.1" }, "yarl": { "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } } } From e5bb92f53c9ad736b4cade4738c8e6727f68a88c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 20 Aug 2020 05:23:28 -0700 Subject: [PATCH 022/680] Disable string splitting/merging by default (#1609) * put experimental string stuff behind a flag * update tests * don't need an output section if it's the same as the input * Primer: Expect no formatting changes in attrs, hypothesis and poetry with --experimental-string-processing off Co-authored-by: Hugo van Kemenade --- src/black/__init__.py | 86 ++++--- src/black_primer/primer.json | 6 +- tests/data/long_strings_flag_disabled.py | 278 +++++++++++++++++++++++ tests/test_black.py | 172 +++++++------- 4 files changed, 420 insertions(+), 122 deletions(-) create mode 100644 tests/data/long_strings_flag_disabled.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 391233ed448..2613b2f9e16 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -240,6 +240,7 @@ class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) line_length: int = DEFAULT_LINE_LENGTH string_normalization: bool = True + experimental_string_processing: bool = False is_pyi: bool = False def get_cache_key(self) -> str: @@ -376,6 +377,15 @@ def target_version_option_callback( is_flag=True, help="Don't normalize string quotes or prefixes.", ) +@click.option( + "--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." + ), +) @click.option( "--check", is_flag=True, @@ -485,6 +495,7 @@ def main( fast: bool, pyi: bool, skip_string_normalization: bool, + experimental_string_processing: bool, quiet: bool, verbose: bool, include: str, @@ -505,6 +516,7 @@ def main( line_length=line_length, is_pyi=pyi, string_normalization=not skip_string_normalization, + experimental_string_processing=experimental_string_processing, ) if config and verbose: out(f"Using configuration from {config}.", bold=False, fg="blue") @@ -984,10 +996,7 @@ def f( before, after = elt.maybe_empty_lines(current_line) dst_contents.append(str(empty_line) * before) for line in transform_line( - current_line, - line_length=mode.line_length, - normalize_strings=mode.string_normalization, - features=split_line_features, + current_line, mode=mode, features=split_line_features ): dst_contents.append(str(line)) return "".join(dst_contents) @@ -2649,10 +2658,7 @@ def make_comment(content: str) -> str: def transform_line( - line: Line, - line_length: int, - normalize_strings: bool, - features: Collection[Feature] = (), + line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: """Transform a `line`, potentially splitting it into many lines. @@ -2668,7 +2674,7 @@ def transform_line( def init_st(ST: Type[StringTransformer]) -> StringTransformer: """Initialize StringTransformer""" - return ST(line_length, normalize_strings) + return ST(mode.line_length, mode.string_normalization) string_merge = init_st(StringMerger) string_paren_strip = init_st(StringParenStripper) @@ -2681,21 +2687,26 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: and not line.should_explode and not line.is_collection_with_optional_trailing_comma and ( - is_line_short_enough(line, line_length=line_length, line_str=line_str) + is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() ) and not (line.contains_standalone_comments() and line.inside_brackets) ): # Only apply basic string preprocessing, since lines shouldn't be split here. - transformers = [string_merge, string_paren_strip] + if mode.experimental_string_processing: + transformers = [string_merge, string_paren_strip] + else: + transformers = [] elif line.is_def: transformers = [left_hand_split] else: def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: - for omit in generate_trailers_to_omit(line, line_length): - lines = list(right_hand_split(line, line_length, features, omit=omit)) - if is_line_short_enough(lines[0], line_length=line_length): + for omit in generate_trailers_to_omit(line, mode.line_length): + lines = list( + right_hand_split(line, mode.line_length, features, omit=omit) + ) + if is_line_short_enough(lines[0], line_length=mode.line_length): yield from lines return @@ -2706,24 +2717,30 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: # See #762 and #781 for the full story. yield from right_hand_split(line, line_length=1, features=features) - if line.inside_brackets: - transformers = [ - string_merge, - string_paren_strip, - delimiter_split, - standalone_comment_split, - string_split, - string_paren_wrap, - rhs, - ] + if mode.experimental_string_processing: + if line.inside_brackets: + transformers = [ + string_merge, + string_paren_strip, + delimiter_split, + standalone_comment_split, + string_split, + string_paren_wrap, + rhs, + ] + else: + transformers = [ + string_merge, + string_paren_strip, + string_split, + string_paren_wrap, + rhs, + ] else: - transformers = [ - string_merge, - string_paren_strip, - string_split, - string_paren_wrap, - rhs, - ] + if line.inside_brackets: + transformers = [delimiter_split, standalone_comment_split, rhs] + else: + transformers = [rhs] for transform in transformers: # We are accumulating lines in `result` because we might want to abort @@ -2738,12 +2755,7 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: ) result.extend( - transform_line( - transformed_line, - line_length=line_length, - normalize_strings=normalize_strings, - features=features, - ) + transform_line(transformed_line, mode=mode, features=features) ) except CannotTransform: continue diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index f5cc3fdf931..7d8271188a2 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -10,7 +10,7 @@ }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] @@ -47,7 +47,7 @@ }, "hypothesis": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] @@ -63,7 +63,7 @@ }, "poetry": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-poetry/poetry.git", "long_checkout": false, "py_versions": ["all"] diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/long_strings_flag_disabled.py new file mode 100644 index 00000000000..1ea864d4bd5 --- /dev/null +++ b/tests/data/long_strings_flag_disabled.py @@ -0,0 +1,278 @@ +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." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "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 dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "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 dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "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 dictionary.", +} + +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), +} + +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 = ( + "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?" +) + +bad_split2 = ( + "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." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +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.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="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?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +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" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %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." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +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 #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 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_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.", # 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." + ), +) + +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 +) + +func_with_bad_parens( + ("short string that should have parens stripped"), x, y, z, +) + +func_with_bad_parens( + x, y, ("short string that should have parens stripped"), z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + 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." + +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 \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "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 = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {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 four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/tests/test_black.py b/tests/test_black.py index 3c766330a08..686232a7f9c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -3,6 +3,7 @@ import logging from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager +from dataclasses import replace from functools import partial from io import BytesIO, TextIOWrapper import os @@ -36,8 +37,9 @@ from .test_primer import PrimerCLITests # noqa: F401 -ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True) -fs = partial(black.format_str, mode=black.FileMode()) +DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) +fs = partial(black.format_str, mode=DEFAULT_MODE) THIS_FILE = Path(__file__) THIS_DIR = THIS_FILE.parent PROJECT_ROOT = THIS_DIR.parent @@ -190,13 +192,13 @@ def invokeBlack( ) @patch("black.dump_to_file", dump_to_stderr) - def checkSourceFile(self, name: str) -> None: + def checkSourceFile(self, name: str, mode: black.FileMode = DEFAULT_MODE) -> None: path = THIS_DIR.parent / name source, expected = read_data(str(path), data=False) - actual = fs(source) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, mode) self.assertFalse(ff(path)) @patch("black.dump_to_file", dump_to_stderr) @@ -205,7 +207,7 @@ def test_empty(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_empty_ff(self) -> None: expected = "" @@ -267,7 +269,7 @@ def test_piping(self) -> None: self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) black.assert_equivalent(source, result.output) - black.assert_stable(source, result.output, black.FileMode()) + black.assert_stable(source, result.output, DEFAULT_MODE) def test_piping_diff(self) -> None: diff_header = re.compile( @@ -320,7 +322,7 @@ def test_function(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_function2(self) -> None: @@ -328,7 +330,7 @@ def test_function2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_function_trailing_comma(self) -> None: @@ -336,7 +338,7 @@ def test_function_trailing_comma(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_expression(self) -> None: @@ -344,14 +346,14 @@ def test_expression(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + 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, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) if sys.version_info >= (3, 8): black.assert_equivalent(source, actual) @@ -375,7 +377,7 @@ def test_expression_ff(self) -> None: self.assertFormatEqual(expected, actual) with patch("black.dump_to_file", dump_to_stderr): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_expression_diff(self) -> None: source, _ = read_data("expression.py") @@ -427,14 +429,14 @@ def test_fstring(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @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, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) if sys.version_info >= (3, 8): black.assert_equivalent(source, actual) @@ -452,8 +454,8 @@ def test_string_quotes(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - mode = black.FileMode(string_normalization=False) + black.assert_stable(source, actual, DEFAULT_MODE) + mode = replace(DEFAULT_MODE, string_normalization=False) not_normalized = fs(source, mode=mode) self.assertFormatEqual(source.replace("\\\n", ""), not_normalized) black.assert_equivalent(source, not_normalized) @@ -465,7 +467,7 @@ def test_docstring(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_long_strings(self) -> None: """Tests for splitting long strings.""" @@ -473,7 +475,15 @@ def test_long_strings(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_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_long_strings__edge_case(self) -> None: @@ -482,7 +492,7 @@ def test_long_strings__edge_case(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_long_strings__regression(self) -> None: @@ -491,7 +501,7 @@ def test_long_strings__regression(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_slices(self) -> None: @@ -499,7 +509,7 @@ def test_slices(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_percent_precedence(self) -> None: @@ -507,7 +517,7 @@ def test_percent_precedence(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments(self) -> None: @@ -515,7 +525,7 @@ def test_comments(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments2(self) -> None: @@ -523,7 +533,7 @@ def test_comments2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments3(self) -> None: @@ -531,7 +541,7 @@ def test_comments3(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments4(self) -> None: @@ -539,7 +549,7 @@ def test_comments4(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments5(self) -> None: @@ -547,7 +557,7 @@ def test_comments5(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments6(self) -> None: @@ -555,7 +565,7 @@ def test_comments6(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments7(self) -> None: @@ -563,7 +573,7 @@ def test_comments7(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comment_after_escaped_newline(self) -> None: @@ -571,7 +581,7 @@ def test_comment_after_escaped_newline(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_cantfit(self) -> None: @@ -579,7 +589,7 @@ def test_cantfit(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_import_spacing(self) -> None: @@ -587,7 +597,7 @@ def test_import_spacing(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_composition(self) -> None: @@ -595,7 +605,7 @@ def test_composition(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_empty_lines(self) -> None: @@ -603,7 +613,7 @@ def test_empty_lines(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_remove_parens(self) -> None: @@ -611,7 +621,7 @@ def test_remove_parens(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_string_prefixes(self) -> None: @@ -619,12 +629,12 @@ def test_string_prefixes(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_numeric_literals(self) -> None: source, expected = read_data("numeric_literals") - mode = black.FileMode(target_versions=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -633,7 +643,7 @@ def test_numeric_literals(self) -> None: @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 = black.FileMode(target_versions=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -644,7 +654,7 @@ def test_numeric_literals_py2(self) -> None: source, expected = read_data("numeric_literals_py2") actual = fs(source) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_python2(self) -> None: @@ -652,12 +662,12 @@ def test_python2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: source, expected = read_data("python2_print_function") - mode = black.FileMode(target_versions={TargetVersion.PY27}) + mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27}) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -669,11 +679,11 @@ def test_python2_unicode_literals(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_stub(self) -> None: - mode = black.FileMode(is_pyi=True) + mode = replace(DEFAULT_MODE, is_pyi=True) source, expected = read_data("stub.pyi") actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) @@ -688,7 +698,7 @@ def test_async_as_identifier(self) -> None: major, minor = sys.version_info[:2] if major < 3 or (major <= 3 and minor < 7): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) # ensure black can parse this when the target is 3.6 self.invokeBlack([str(source_path), "--target-version", "py36"]) # but not on 3.7, because async/await is no longer an identifier @@ -703,7 +713,7 @@ def test_python37(self) -> None: major, minor = sys.version_info[:2] if major > 3 or (major == 3 and minor >= 7): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) # ensure black can parse this when the target is 3.7 self.invokeBlack([str(source_path), "--target-version", "py37"]) # but not on 3.6, because we use async as a reserved keyword @@ -717,7 +727,7 @@ def test_python38(self) -> None: 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, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff(self) -> None: @@ -725,7 +735,7 @@ def test_fmtonoff(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff2(self) -> None: @@ -733,7 +743,7 @@ def test_fmtonoff2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff3(self) -> None: @@ -741,7 +751,7 @@ def test_fmtonoff3(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff4(self) -> None: @@ -749,7 +759,7 @@ def test_fmtonoff4(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_remove_empty_parentheses_after_class(self) -> None: @@ -757,7 +767,7 @@ def test_remove_empty_parentheses_after_class(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_new_line_between_class_and_code(self) -> None: @@ -765,7 +775,7 @@ def test_new_line_between_class_and_code(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_bracket_match(self) -> None: @@ -773,7 +783,7 @@ def test_bracket_match(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_tuple_assign(self) -> None: @@ -781,7 +791,7 @@ def test_tuple_assign(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_beginning_backslash(self) -> None: @@ -789,7 +799,7 @@ def test_beginning_backslash(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + 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" @@ -1218,7 +1228,7 @@ def err(msg: str, **kwargs: Any) -> None: def test_format_file_contents(self) -> None: empty = "" - mode = black.FileMode() + mode = DEFAULT_MODE with self.assertRaises(black.NothingChanged): black.format_file_contents(empty, mode=mode, fast=False) just_nl = "\n" @@ -1263,7 +1273,7 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual("".join(err_lines), "") def test_cache_broken_file(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: cache_file = black.get_cache_file(mode) with cache_file.open("w") as fobj: @@ -1277,7 +1287,7 @@ def test_cache_broken_file(self) -> None: self.assertIn(src, cache) def test_cache_single_file_already_cached(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: @@ -1289,7 +1299,7 @@ def test_cache_single_file_already_cached(self) -> None: @event_loop() def test_cache_multiple_files(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -1310,7 +1320,7 @@ def test_cache_multiple_files(self) -> None: self.assertIn(two, cache) def test_no_cache_when_writeback_diff(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: @@ -1320,7 +1330,7 @@ def test_no_cache_when_writeback_diff(self) -> None: self.assertFalse(cache_file.exists()) def test_no_cache_when_stdin(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(): result = CliRunner().invoke( black.main, ["-"], input=BytesIO(b"print('hello')") @@ -1330,12 +1340,12 @@ def test_no_cache_when_stdin(self) -> None: self.assertFalse(cache_file.exists()) def test_read_cache_no_cachefile(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(): self.assertEqual(black.read_cache(mode), {}) def test_write_cache_read_cache(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.touch() @@ -1361,7 +1371,7 @@ def test_filter_cached(self) -> None: self.assertEqual(done, {cached}) def test_write_cache_creates_directory_if_needed(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(exists=False) as workspace: self.assertFalse(workspace.exists()) black.write_cache({}, [], mode) @@ -1369,7 +1379,7 @@ def test_write_cache_creates_directory_if_needed(self) -> None: @event_loop() def test_failed_formatting_does_not_get_cached(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -1385,7 +1395,7 @@ def test_failed_formatting_does_not_get_cached(self) -> None: self.assertIn(clean, cache) def test_write_cache_write_fail(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(), patch.object(Path, "open") as mock: mock.side_effect = OSError black.write_cache({}, [], mode) @@ -1428,8 +1438,8 @@ def test_broken_symlink(self) -> None: self.invokeBlack([str(workspace.resolve())]) def test_read_cache_line_lengths(self) -> None: - mode = black.FileMode() - short_mode = black.FileMode(line_length=1) + mode = DEFAULT_MODE + short_mode = replace(DEFAULT_MODE, line_length=1) with cache_dir() as workspace: path = (workspace / "file.py").resolve() path.touch() @@ -1444,11 +1454,11 @@ def test_tricky_unicode_symbols(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_single_file_force_pyi(self) -> None: - reg_mode = black.FileMode() - pyi_mode = black.FileMode(is_pyi=True) + reg_mode = DEFAULT_MODE + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: path = (workspace / "file.py").resolve() @@ -1466,8 +1476,8 @@ def test_single_file_force_pyi(self) -> None: @event_loop() def test_multi_file_force_pyi(self) -> None: - reg_mode = black.FileMode() - pyi_mode = black.FileMode(is_pyi=True) + reg_mode = DEFAULT_MODE + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: paths = [ @@ -1499,8 +1509,8 @@ def test_pipe_force_pyi(self) -> None: self.assertFormatEqual(actual, expected) def test_single_file_force_py36(self) -> None: - reg_mode = black.FileMode() - py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) + reg_mode = DEFAULT_MODE + py36_mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() @@ -1518,8 +1528,8 @@ def test_single_file_force_py36(self) -> None: @event_loop() def test_multi_file_force_py36(self) -> None: - reg_mode = black.FileMode() - py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) + reg_mode = DEFAULT_MODE + py36_mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: paths = [ @@ -1546,7 +1556,7 @@ def test_collections(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") @@ -1827,9 +1837,7 @@ def test_parse_pyproject_toml(self) -> None: def test_read_pyproject_toml(self) -> None: test_toml_file = THIS_DIR / "test.toml" fake_ctx = FakeContext() - black.read_pyproject_toml( - fake_ctx, FakeParameter(), str(test_toml_file), - ) + black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file)) config = fake_ctx.default_map self.assertEqual(config["verbose"], "1") self.assertEqual(config["check"], "no") From 37a0020e073555ffe0921ec1356a27610aadcca4 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 20 Aug 2020 18:06:41 -0400 Subject: [PATCH 023/680] Upgrade docs to Sphinx 3+ and add doc build test (#1613) * Upgrade docs to Sphinx 3+ * Fix all the warnings... - Fixed bad docstrings - Fixed bad fenced code blocks in documentation - Blocklisted some sections from being generated from the README - Added missing documentation to index.rst - Fixed an invalid autofunction directive in reference/reference_functions.rst - Pin another documentation dependency * Add documentation build test --- .github/workflows/doc.yml | 36 +++++++++++++++++++++++++ README.md | 4 +-- docs/black_primer.md | 6 ++--- docs/compatible_configs.md | 2 +- docs/conf.py | 37 ++++++++++++++++++-------- docs/index.rst | 3 ++- docs/reference/reference_functions.rst | 2 +- docs/requirements.txt | 5 ++-- src/black/__init__.py | 1 + 9 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/doc.yml diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 00000000000..6023a02a7f7 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,36 @@ +name: Documentation Build + +on: + push: + paths: + - "docs/**" + - "README.md" + - "CHANGES.md" + - "CONTRIBUTING.md" + pull_request: + paths: + - "docs/**" + - "README.md" + - "CHANGES.md" + - "CONTRIBUTING.md" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install -e "." + python -m pip install -r "docs/requirements.txt" + + - name: Build documentation + run: sphinx-build -a -b html -W docs/ docs/_build/ diff --git a/README.md b/README.md index 37b93c143a2..44f2d207c8b 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,7 @@ Twisted and CPython: Use the badge in your project's README.md: -```markdown +```md [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ``` @@ -625,7 +625,7 @@ Multiple contributions by: - [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) - MomIsBestFriend - [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) -- [Nathan Hunt](mailtoneighthan.hunt@gmail.com) +- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) - [Neraste](mailto:neraste.herr10@gmail.com) - [Nikolaus Waxweiler](mailto:madigens@gmail.com) - [Ofek Lev](mailto:ofekmeister@gmail.com) diff --git a/docs/black_primer.md b/docs/black_primer.md index af4184233e2..a2dd964b7dc 100644 --- a/docs/black_primer.md +++ b/docs/black_primer.md @@ -71,7 +71,7 @@ each parameter is explained below: "expect_formatting_changes": true, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, - "py_versions": ["all", "3.8"] // "all" ignores all other versions + "py_versions": ["all", "3.8"] } } } @@ -103,9 +103,9 @@ Failed projects: +++ tests/b303_b304.py 2020-05-17 20:06:42.753851 +0000 @@ -26,11 +26,11 @@ maxint = 5 # this is okay - # the following shouldn't crash + # the following should not crash (a, b, c) = list(range(3)) - # it's different than this + # it is different than this a, b, c = list(range(3)) - a, b, c, = list(range(3)) + a, b, c = list(range(3)) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index 723fc889c00..25e959e3281 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -241,7 +241,7 @@ characters via `max-line-length = 88`.
pylintrc -```rc +```ini [MESSAGES CONTROL] disable = C0330, C0326 diff --git a/docs/conf.py b/docs/conf.py index 3343087cfbd..575a14011e4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,18 +17,15 @@ import string from typing import Callable, List, Optional, Pattern, Tuple, Set from dataclasses import dataclass -import os import logging from pkg_resources import get_distribution -from recommonmark.parser import CommonMarkParser logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO) LOG = logging.getLogger(__name__) -# Get a relative path so logs printing out SRC isn't too long. -CURRENT_DIR = Path(__file__).parent.relative_to(os.getcwd()) +CURRENT_DIR = Path(__file__).parent README = CURRENT_DIR / ".." / "README.md" REFERENCE_DIR = CURRENT_DIR / "reference" STATIC_DIR = CURRENT_DIR / "_static" @@ -200,7 +197,7 @@ def process_sections( # -- Project information ----------------------------------------------------- project = "Black" -copyright = "2018, Łukasz Langa and contributors to Black" +copyright = "2020, Łukasz Langa and contributors to Black" author = "Łukasz Langa and contributors to Black" # Autopopulate version @@ -213,7 +210,6 @@ def process_sections( custom_sections = [ DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md",), - DocSection("pragmatism", CURRENT_DIR / "the_black_code_style.md",), DocSection("editor_integration", CURRENT_DIR / "editor_integration.md"), DocSection("blackd", CURRENT_DIR / "blackd.md"), DocSection("black_primer", CURRENT_DIR / "black_primer.md"), @@ -221,28 +217,47 @@ def process_sections( DocSection("change_log", CURRENT_DIR / ".." / "CHANGES.md"), ] +# Sphinx complains when there is a source file that isn't referenced in any of the docs. +# Since some sections autogenerated from the README are unused warnings will appear. +# +# Sections must be listed to what their name is when passed through make_filename(). +blocklisted_sections_from_readme = { + "license", + "pragmatism", + "testimonials", + "used_by", +} make_pypi_svg(release) readme_sections = get_sections_from_readme() +readme_sections = [ + x for x in readme_sections if x.name not in blocklisted_sections_from_readme +] + process_sections(custom_sections, readme_sections) # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' +needs_sphinx = "3.0" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "recommonmark", +] + +# If you need extensions of a certain version or higher, list them here. +needs_extensions = {"recommonmark": "0.5"} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] -source_parsers = {".md": CommonMarkParser} - # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: source_suffix = [".rst", ".md"] diff --git a/docs/index.rst b/docs/index.rst index 676644ec6c6..f03d247d949 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,10 +51,12 @@ Contents installation_and_usage the_black_code_style pyproject_toml + compatible_configs editor_integration blackd black_primer version_control_integration + github_actions ignoring_unmodified_files contributing_to_black show_your_style @@ -66,5 +68,4 @@ Indices and tables ================== * :ref:`genindex` -* :ref:`modindex` * :ref:`search` diff --git a/docs/reference/reference_functions.rst b/docs/reference/reference_functions.rst index b10eea9b01f..1beecc10325 100644 --- a/docs/reference/reference_functions.rst +++ b/docs/reference/reference_functions.rst @@ -89,7 +89,7 @@ Split functions .. autofunction:: black.standalone_comment_split -.. autofunction:: black.split_line +.. autofunction:: black.transform_line Caching ------- diff --git a/docs/requirements.txt b/docs/requirements.txt index a36fd8a675b..4cad9bc205b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ -recommonmark==0.4.0 -Sphinx==1.7.2 +recommonmark==0.6.0 +Sphinx==3.2.1 +Pygments==2.6.1 \ No newline at end of file diff --git a/src/black/__init__.py b/src/black/__init__.py index 2613b2f9e16..d251136942a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -952,6 +952,7 @@ def f(arg: str = "") -> None: ... A more complex example: + >>> print( ... black.format_str( ... "def f(arg:str='')->None: hey", From 4938cc9e9abf2581cb154c6a8d1ae66eb18e0d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 29 Oct 2019 00:34:46 +0100 Subject: [PATCH 024/680] Reset trailing comma handling --- src/black/__init__.py | 138 ++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 91 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index d251136942a..2250943665a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1494,69 +1494,6 @@ def is_stub_class(self) -> bool: Leaf(token.DOT, ".") for _ in range(3) ] - @property - def is_collection_with_optional_trailing_comma(self) -> bool: - """Is this line a collection literal with a trailing comma that's optional? - - Note that the trailing comma in a 1-tuple is not optional. - """ - if not self.leaves or len(self.leaves) < 4: - return False - - # Look for and address a trailing colon. - if self.leaves[-1].type == token.COLON: - closer = self.leaves[-2] - close_index = -2 - else: - closer = self.leaves[-1] - close_index = -1 - if closer.type not in CLOSING_BRACKETS or self.inside_brackets: - return False - - if closer.type == token.RPAR: - # Tuples require an extra check, because if there's only - # one element in the tuple removing the comma unmakes the - # tuple. - # - # We also check for parens before looking for the trailing - # comma because in some cases (eg assigning a dict - # literal) the literal gets wrapped in temporary parens - # during parsing. This case is covered by the - # collections.py test data. - opener = closer.opening_bracket - for _open_index, leaf in enumerate(self.leaves): - if leaf is opener: - break - - else: - # Couldn't find the matching opening paren, play it safe. - return False - - commas = 0 - comma_depth = self.leaves[close_index - 1].bracket_depth - for leaf in self.leaves[_open_index + 1 : close_index]: - if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA: - commas += 1 - if commas > 1: - # We haven't looked yet for the trailing comma because - # we might also have caught noop parens. - return self.leaves[close_index - 1].type == token.COMMA - - elif commas == 1: - return False # it's either a one-tuple or didn't have a trailing comma - - if self.leaves[close_index - 1].type in CLOSING_BRACKETS: - close_index -= 1 - closer = self.leaves[close_index] - if closer.type == token.RPAR: - # TODO: this is a gut feeling. Will we ever see this? - return False - - if self.leaves[close_index - 1].type != token.COMMA: - return False - - return True - @property def is_def(self) -> bool: """Is this a function definition? (Also returns True for async defs.)""" @@ -1683,40 +1620,60 @@ def contains_multiline_strings(self) -> bool: def maybe_remove_trailing_comma(self, closing: Leaf) -> bool: """Remove trailing comma if there is one and it's safe.""" - if not (self.leaves and self.leaves[-1].type == token.COMMA): - return False - - # We remove trailing commas only in the case of importing a - # single name from a module. if not ( self.leaves - and self.is_import - and len(self.leaves) > 4 and self.leaves[-1].type == token.COMMA and closing.type in CLOSING_BRACKETS - and self.leaves[-4].type == token.NAME - and ( - # regular `from foo import bar,` - self.leaves[-4].value == "import" - # `from foo import (bar as baz,) - or ( - len(self.leaves) > 6 - and self.leaves[-6].value == "import" - and self.leaves[-3].value == "as" - ) - # `from foo import bar as baz,` - or ( - len(self.leaves) > 5 - and self.leaves[-5].value == "import" - and self.leaves[-3].value == "as" - ) - ) - and closing.type == token.RPAR ): return False - self.remove_trailing_comma() - return True + if closing.type == token.RBRACE: + self.remove_trailing_comma() + return True + + if closing.type == token.RSQB: + comma = self.leaves[-1] + if comma.parent and comma.parent.type == syms.listmaker: + self.remove_trailing_comma() + return True + + # For parens let's check if it's safe to remove the comma. + # Imports are always safe. + if self.is_import: + self.remove_trailing_comma() + return True + + # Otherwise, if the trailing one is the only one, we might mistakenly + # change a tuple into a different type by removing the comma. + depth = closing.bracket_depth + 1 + commas = 0 + opening = closing.opening_bracket + for _opening_index, leaf in enumerate(self.leaves): + if leaf is opening: + break + + else: + return False + + for leaf in self.leaves[_opening_index + 1 :]: + if leaf is closing: + break + + bracket_depth = leaf.bracket_depth + if bracket_depth == depth and leaf.type == token.COMMA: + commas += 1 + if leaf.parent and leaf.parent.type in { + syms.arglist, + syms.typedargslist, + }: + commas += 1 + break + + if commas > 1: + self.remove_trailing_comma() + return True + + return False def append_comment(self, comment: Leaf) -> bool: """Add an inline or standalone comment to the line.""" @@ -2686,7 +2643,6 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: if ( not line.contains_uncollapsable_type_comments() and not line.should_explode - and not line.is_collection_with_optional_trailing_comma and ( is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() From 788268bc39a87d37a24d203fa5ee7b3953af3446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 29 Oct 2019 00:50:42 +0100 Subject: [PATCH 025/680] Re-implement magic trailing comma handling: - when a trailing comma is specified in any bracket pair, that signals to Black that this bracket pair needs to be always exploded, e.g. presented as "one item per line"; - this causes some changes to previously formatted code that erroneously left trailing commas embedded into single-line expressions; - internally, Black needs to be able to identify trailing commas that it put itself compared to pre-existing trailing commas. We do this by using/abusing lib2to3's `was_checked` attribute. It's True for internally generated trailing commas and False for pre-existing ones (in fact, for all pre-existing leaves and nodes). Fixes #1288 --- CHANGES.md | 3 + gallery/gallery.py | 5 +- src/black/__init__.py | 126 +++++++++++----------- src/blib2to3/pgen2/driver.py | 2 +- tests/data/collections.py | 35 ++++-- tests/data/comments2.py | 8 +- tests/data/comments7.py | 9 +- tests/data/expression.diff | 27 ++++- tests/data/expression.py | 22 +++- tests/data/fmtonoff4.py | 7 +- tests/data/function.py | 5 +- tests/data/function2.py | 5 +- tests/data/function_trailing_comma.py | 58 ++++++++-- tests/data/function_trailing_comma_wip.py | 5 + tests/data/import_spacing.py | 8 +- tests/data/long_strings.py | 32 +++++- tests/data/long_strings__regression.py | 12 ++- tests/data/long_strings_flag_disabled.py | 18 +++- tests/test_black.py | 52 ++++++++- 19 files changed, 336 insertions(+), 103 deletions(-) create mode 100644 tests/data/function_trailing_comma_wip.py diff --git a/CHANGES.md b/CHANGES.md index 6d418b9bec8..eb6d1c2ebbf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ #### _Black_ +- re-implemented support for explicit trailing commas: now it works consistently within + any bracket pair, including nested structures (#1288 and duplicates) + - reindent docstrings when reindenting code around it (#1053) - show colored diffs (#1266) diff --git a/gallery/gallery.py b/gallery/gallery.py index 2a56b4ed4c0..6b42ec3a6d4 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -127,7 +127,10 @@ def get_package( def download_and_extract_top_packages( - directory: Path, days: Days = 365, workers: int = 8, limit: slice = DEFAULT_SLICE, + 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) diff --git a/src/black/__init__.py b/src/black/__init__.py index 2250943665a..8d0c70f06c7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1442,7 +1442,8 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: ) if self.inside_brackets or not preformatted: self.bracket_tracker.mark(leaf) - self.maybe_remove_trailing_comma(leaf) + if self.maybe_should_explode(leaf): + self.should_explode = True if not self.append_comment(leaf): self.leaves.append(leaf) @@ -1618,59 +1619,26 @@ def contains_unsplittable_type_ignore(self) -> bool: def contains_multiline_strings(self) -> bool: return any(is_multiline_string(leaf) for leaf in self.leaves) - def maybe_remove_trailing_comma(self, closing: Leaf) -> bool: - """Remove trailing comma if there is one and it's safe.""" + def maybe_should_explode(self, closing: Leaf) -> bool: + """Return True if this line should explode (always be split), that is when: + - there's a pre-existing trailing comma here; and + - it's not a one-tuple. + """ if not ( - self.leaves + closing.type in CLOSING_BRACKETS + and self.leaves and self.leaves[-1].type == token.COMMA - and closing.type in CLOSING_BRACKETS + and not self.leaves[-1].was_checked # pre-existing ): return False - if closing.type == token.RBRACE: - self.remove_trailing_comma() + if closing.type in {token.RBRACE, token.RSQB}: return True - if closing.type == token.RSQB: - comma = self.leaves[-1] - if comma.parent and comma.parent.type == syms.listmaker: - self.remove_trailing_comma() - return True - - # For parens let's check if it's safe to remove the comma. - # Imports are always safe. if self.is_import: - self.remove_trailing_comma() return True - # Otherwise, if the trailing one is the only one, we might mistakenly - # change a tuple into a different type by removing the comma. - depth = closing.bracket_depth + 1 - commas = 0 - opening = closing.opening_bracket - for _opening_index, leaf in enumerate(self.leaves): - if leaf is opening: - break - - else: - return False - - for leaf in self.leaves[_opening_index + 1 :]: - if leaf is closing: - break - - bracket_depth = leaf.bracket_depth - if bracket_depth == depth and leaf.type == token.COMMA: - commas += 1 - if leaf.parent and leaf.parent.type in { - syms.arglist, - syms.typedargslist, - }: - commas += 1 - break - - if commas > 1: - self.remove_trailing_comma() + if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves): return True return False @@ -2647,7 +2615,7 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() ) - and not (line.contains_standalone_comments() and line.inside_brackets) + 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: @@ -4772,10 +4740,8 @@ def right_hand_split( tail = bracket_split_build_line(tail_leaves, line, opening_bracket) bracket_split_succeeded_or_raise(head, body, tail) if ( - # the body shouldn't be exploded - not body.should_explode # the opening bracket is an optional paren - and opening_bracket.type == token.LPAR + opening_bracket.type == token.LPAR and not opening_bracket.value # the closing bracket is an optional paren and closing_bracket.type == token.RPAR @@ -4872,7 +4838,9 @@ def bracket_split_build_line( continue if leaves[i].type != token.COMMA: - leaves.insert(i + 1, Leaf(token.COMMA, ",")) + new_comma = Leaf(token.COMMA, ",") + new_comma.was_checked = True + leaves.insert(i + 1, new_comma) break # Populate the line @@ -4880,8 +4848,8 @@ def bracket_split_build_line( result.append(leaf, preformatted=True) for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) - if is_body: - result.should_explode = should_explode(result, opening_bracket) + if is_body and should_split_body_explode(result, opening_bracket): + result.should_explode = True return result @@ -4966,7 +4934,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: and current_line.leaves[-1].type != token.COMMA and current_line.leaves[-1].type != STANDALONE_COMMENT ): - current_line.append(Leaf(token.COMMA, ",")) + new_comma = Leaf(token.COMMA, ",") + new_comma.was_checked = True + current_line.append(new_comma) yield current_line @@ -5588,24 +5558,60 @@ def ensure_visible(leaf: Leaf) -> None: leaf.value = ")" -def should_explode(line: Line, opening_bracket: Leaf) -> bool: +def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: """Should `line` immediately be split with `delimiter_split()` after RHS?""" - if not ( - opening_bracket.parent - and opening_bracket.parent.type in {syms.atom, syms.import_from} - and opening_bracket.value in "[{(" - ): + if not (opening_bracket.parent and opening_bracket.value in "[{("): return False + # We're essentially checking if the body is delimited by commas and there's more + # than one of them (we're excluding the trailing comma and if the delimiter priority + # is still commas, that means there's more). + exclude = set() + pre_existing_trailing_comma = False try: last_leaf = line.leaves[-1] - exclude = {id(last_leaf)} if last_leaf.type == token.COMMA else set() + if last_leaf.type == token.COMMA: + pre_existing_trailing_comma = not last_leaf.was_checked + exclude.add(id(last_leaf)) max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) except (IndexError, ValueError): return False - return max_priority == COMMA_PRIORITY + return max_priority == COMMA_PRIORITY and ( + # always explode imports + opening_bracket.parent.type in {syms.atom, syms.import_from} + or pre_existing_trailing_comma + ) + + +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.""" + depth = closing.bracket_depth + 1 + for _opening_index, leaf in enumerate(leaves): + if leaf is opening: + break + + else: + raise LookupError("Opening paren not found in `leaves`") + + commas = 0 + _opening_index += 1 + for leaf in leaves[_opening_index:]: + if leaf is closing: + break + + bracket_depth = leaf.bracket_depth + if bracket_depth == depth and leaf.type == token.COMMA: + commas += 1 + if leaf.parent and leaf.parent.type in { + syms.arglist, + syms.typedargslist, + }: + commas += 1 + break + + return commas < 2 def get_features_used(node: Node) -> Set[Feature]: diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 052c94883cf..81940f78f0f 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -128,7 +128,7 @@ def parse_stream(self, stream: IO[Text], debug: bool = False) -> NL: 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[Text] = None, debug: bool = False ) -> NL: """Parse a file and return the syntax tree.""" with io.open(filename, "r", encoding=encoding) as stream: diff --git a/tests/data/collections.py b/tests/data/collections.py index ebe8d3c5200..68431665211 100644 --- a/tests/data/collections.py +++ b/tests/data/collections.py @@ -2,18 +2,18 @@ from . import A, B, C -# unwraps +# keeps existing trailing comma from foo import ( bar, ) -# stays wrapped +# also keeps existing structure from foo import ( baz, qux, ) -# as doesn't get confusing when unwrapped +# `as` works as well from foo import ( xyzzy as magic, ) @@ -77,17 +77,21 @@ from . import A, B, C -# unwraps -from foo import bar +# keeps existing trailing comma +from foo import ( + bar, +) -# stays wrapped +# also keeps existing structure from foo import ( baz, qux, ) -# as doesn't get confusing when unwrapped -from foo import xyzzy as magic +# `as` works as well +from foo import ( + xyzzy as magic, +) a = { 1, @@ -151,11 +155,20 @@ if True: ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,} + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) diff --git a/tests/data/comments2.py b/tests/data/comments2.py index 89c29104bd8..221cb3fe143 100644 --- a/tests/data/comments2.py +++ b/tests/data/comments2.py @@ -316,7 +316,13 @@ def inline_comments_in_brackets_ruin_everything(): ) -CONFIG_FILES = [CONFIG_FILE,] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = ( + [ + CONFIG_FILE, + ] + + SHARED_CONFIG_FILES + + USER_CONFIG_FILES +) # type: Final class Test: diff --git a/tests/data/comments7.py b/tests/data/comments7.py index 436df1a2a41..a7bd281c91e 100644 --- a/tests/data/comments7.py +++ b/tests/data/comments7.py @@ -97,7 +97,14 @@ def func(): def func(): - c = call(0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1],) # type: ignore + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0789, + a[-1], # type: ignore + ) # The type: ignore exception only applies to line length, not # other types of formatting. diff --git a/tests/data/expression.diff b/tests/data/expression.diff index f47ee1c6d2c..684f92cd3b7 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -130,15 +130,21 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -94,23 +127,25 @@ +@@ -94,26 +127,29 @@ 1.0 .real ....__class__ list[str] dict[str, int] tuple[str, ...] ++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], ++ str, ++ int, ++ float, ++ dict[str, int], +] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -160,7 +166,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -134,112 +169,170 @@ +@@ -137,113 +173,180 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -200,6 +206,7 @@ g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove) +-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all() -result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all() +what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( + vars_to_remove @@ -212,7 +219,17 @@ + .filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ) -+ .order_by(models.Customer.id.asc(),) ++ .order_by(models.Customer.id.asc()) ++ .all() ++) ++result = ( ++ session.query(models.Customer.id) ++ .filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ) ++ .order_by( ++ models.Customer.id.asc(), ++ ) + .all() +) Ø = set() diff --git a/tests/data/expression.py b/tests/data/expression.py index 6a04db8b1c4..8e63bdcdf9b 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -96,6 +96,9 @@ list[str] dict[str, int] tuple[str, ...] +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]]]], @@ -157,6 +160,7 @@ g = 1, *"ten" what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove) what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove) +result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all() result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all() Ø = set() authors.łukasz.say_thanks() @@ -379,8 +383,12 @@ async def f(): list[str] dict[str, int] tuple[str, ...] +tuple[str, int, float, dict[str, int]] tuple[ - str, int, float, dict[str, int], + str, + int, + float, + dict[str, int], ] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -459,7 +467,17 @@ async def f(): .filter( models.Customer.account_id == account_id, models.Customer.email == email_address ) - .order_by(models.Customer.id.asc(),) + .order_by(models.Customer.id.asc()) + .all() +) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ) + .order_by( + models.Customer.id.asc(), + ) .all() ) Ø = set() diff --git a/tests/data/fmtonoff4.py b/tests/data/fmtonoff4.py index 54673c06b2d..4ca707965ad 100644 --- a/tests/data/fmtonoff4.py +++ b/tests/data/fmtonoff4.py @@ -25,7 +25,12 @@ def f(): @test( - [1, 2, 3, 4,] + [ + 1, + 2, + 3, + 4, + ] ) def f(): pass diff --git a/tests/data/function.py b/tests/data/function.py index 51234a1e9b4..2d642c8731b 100644 --- a/tests/data/function.py +++ b/tests/data/function.py @@ -230,7 +230,10 @@ def trailing_comma(): } -def f(a, **kwargs,) -> A: +def f( + a, + **kwargs, +) -> A: return ( yield from A( very_long_argument_name1=very_long_value_for_the_argument, diff --git a/tests/data/function2.py b/tests/data/function2.py index a6773d429cd..cfc259ea7bd 100644 --- a/tests/data/function2.py +++ b/tests/data/function2.py @@ -25,7 +25,10 @@ def inner(): # output -def f(a, **kwargs,) -> A: +def f( + a, + **kwargs, +) -> A: with cache_dir(): if something: result = CliRunner().invoke( diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index fcd81ad7d96..314a56cf67b 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -1,25 +1,67 @@ def f(a,): - ... + d = {'key': 'value',} + tup = (1,) + +def f2(a,b,): + d = {'key': 'value', 'key2': 'value2',} + tup = (1,2,) def f(a:int=1,): - ... + call(arg={'explode': 'this',}) + call2(arg=[1,2,3],) def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: - pass + json = {"k": {"k2": {"k3": [1,]}}} # output -def f(a,): - ... +def f( + a, +): + d = { + "key": "value", + } + tup = (1,) + + +def f2( + a, + b, +): + d = { + "key": "value", + "key2": "value2", + } + tup = ( + 1, + 2, + ) -def f(a: int = 1,): - ... +def f( + a: int = 1, +): + call( + arg={ + "explode": "this", + } + ) + call2( + arg=[1, 2, 3], + ) def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: - pass + json = { + "k": { + "k2": { + "k3": [ + 1, + ] + } + } + } \ No newline at end of file diff --git a/tests/data/function_trailing_comma_wip.py b/tests/data/function_trailing_comma_wip.py new file mode 100644 index 00000000000..c41fc709d97 --- /dev/null +++ b/tests/data/function_trailing_comma_wip.py @@ -0,0 +1,5 @@ +CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final + +# output + +CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final \ No newline at end of file diff --git a/tests/data/import_spacing.py b/tests/data/import_spacing.py index 51cfda23ff3..8e6e23cc348 100644 --- a/tests/data/import_spacing.py +++ b/tests/data/import_spacing.py @@ -2,6 +2,9 @@ # flake8: noqa +from logging import ( + WARNING +) from logging import ( ERROR, ) @@ -53,7 +56,10 @@ # flake8: noqa -from logging import ERROR +from logging import WARNING +from logging import ( + ERROR, +) import sys # This relies on each of the submodules having an __all__ variable. diff --git a/tests/data/long_strings.py b/tests/data/long_strings.py index 5da460b65c0..e1ed90f22de 100644 --- a/tests/data/long_strings.py +++ b/tests/data/long_strings.py @@ -137,6 +137,20 @@ ), # comment after comma ) +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), + x, + y, + z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, + y, + ("short string that should have parens stripped"), + z +) + func_with_bad_parens( ("short string that should have parens stripped"), x, @@ -487,12 +501,26 @@ def foo(): " which should NOT be there.", # comment after comma ) +func_with_bad_parens_that_wont_fit_in_one_line( + "short string that should have parens stripped", x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, "short string that should have parens stripped", z +) + func_with_bad_parens( - "short string that should have parens stripped", x, y, z, + "short string that should have parens stripped", + x, + y, + z, ) func_with_bad_parens( - x, y, "short string that should have parens stripped", z, + x, + y, + "short string that should have parens stripped", + z, ) annotated_variable: Final = ( diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 8dbc58a4315..044bb4a5deb 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -528,17 +528,23 @@ def xxxx_xxx_xx_xxxxxxxxxx_xxxx_xxxxxxxxx(xxxx): xxxxxxxx = [ xxxxxxxxxxxxxxxx( "xxxx", - xxxxxxxxxxx={"xxxx": 1.0,}, + xxxxxxxxxxx={ + "xxxx": 1.0, + }, xxxxxx={"xxxxxx 1": xxxxxx(xxxx="xxxxxx 1", xxxxxx=600.0)}, xxxxxxxx_xxxxxxx=0.0, ), xxxxxxxxxxxxxxxx( "xxxxxxx", - xxxxxxxxxxx={"xxxx": 1.0,}, + xxxxxxxxxxx={ + "xxxx": 1.0, + }, xxxxxx={"xxxxxx 1": xxxxxx(xxxx="xxxxxx 1", xxxxxx=200.0)}, xxxxxxxx_xxxxxxx=0.0, ), - xxxxxxxxxxxxxxxx("xxxx",), + xxxxxxxxxxxxxxxx( + "xxxx", + ), ] diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/long_strings_flag_disabled.py index 1ea864d4bd5..db3954e3abd 100644 --- a/tests/data/long_strings_flag_disabled.py +++ b/tests/data/long_strings_flag_disabled.py @@ -225,12 +225,26 @@ ), # comment after comma ) +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + func_with_bad_parens( - ("short string that should have parens stripped"), x, y, z, + ("short string that should have parens stripped"), + x, + y, + z, ) func_with_bad_parens( - x, y, ("short string that should have parens stripped"), z, + x, + y, + ("short string that should have parens stripped"), + z, ) annotated_variable: Final = ( diff --git a/tests/test_black.py b/tests/test_black.py index 686232a7f9c..7793b0e1131 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -5,13 +5,25 @@ from contextlib import contextmanager from dataclasses import replace from functools import partial +import inspect from io import BytesIO, TextIOWrapper import os from pathlib import Path import regex as re import sys from tempfile import TemporaryDirectory -from typing import Any, BinaryIO, Dict, Generator, List, Tuple, Iterator, TypeVar +import types +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + List, + Tuple, + Iterator, + TypeVar, +) import unittest from unittest.mock import patch, MagicMock @@ -153,6 +165,7 @@ def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None class BlackTestCase(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"): @@ -171,7 +184,7 @@ def assertFormatEqual(self, expected: str, actual: str) -> None: list(bdv.visit(exp_node)) except Exception as ve: black.err(str(ve)) - self.assertEqual(expected, actual) + self.assertMultiLineEqual(expected, actual) def invokeBlack( self, args: List[str], exit_code: int = 0, ignore_config: bool = True @@ -332,6 +345,16 @@ def test_function2(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) + @patch("black.dump_to_file", dump_to_stderr) + def test_function_trailing_comma_wip(self) -> None: + source, expected = read_data("function_trailing_comma_wip") + # sys.settrace(tracefunc) + actual = fs(source) + # sys.settrace(None) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + @patch("black.dump_to_file", dump_to_stderr) def test_function_trailing_comma(self) -> None: source, expected = read_data("function_trailing_comma") @@ -2039,5 +2062,30 @@ async def test_blackd_response_black_version_header(self) -> None: self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) +with open(black.__file__, "r") 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 + 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 + + if __name__ == "__main__": unittest.main(module="test_black") From 05cc7ede6a6e4f07e621fdd7a3541a5d39f6f156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 20 Aug 2020 19:04:56 +0200 Subject: [PATCH 026/680] Reformat docs/conf.py according to the new style --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 575a14011e4..9e6a9bcaa62 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -209,7 +209,7 @@ def process_sections( version = version.split(sp)[0] custom_sections = [ - DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md",), + DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md"), DocSection("editor_integration", CURRENT_DIR / "editor_integration.md"), DocSection("blackd", CURRENT_DIR / "blackd.md"), DocSection("black_primer", CURRENT_DIR / "black_primer.md"), From bc71685d229baa28fdee8bb1928b82e9f632e0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 20 Aug 2020 19:21:40 +0200 Subject: [PATCH 027/680] Open file explicitly with UTF-8 so it works on Windows, too --- 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 7793b0e1131..f89f1403aba 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2062,7 +2062,7 @@ async def test_blackd_response_black_version_header(self) -> None: self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) -with open(black.__file__, "r") as _bf: +with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From f3ab907a964e81c35cc6a8530e911c897f5a895e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 21 Aug 2020 15:27:08 +0200 Subject: [PATCH 028/680] Mark Primer projects that will change formatting --- src/black_primer/primer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 7d8271188a2..83a9cb50161 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -3,21 +3,21 @@ "projects": { "aioexabgp": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, "py_versions": ["all"] }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] }, "bandersnatch": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pypa/bandersnatch.git", "long_checkout": false, "py_versions": ["all"] @@ -40,14 +40,14 @@ }, "flake8-bugbear": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", "long_checkout": false, "py_versions": ["all"] }, "hypothesis": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] @@ -63,7 +63,7 @@ }, "poetry": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-poetry/poetry.git", "long_checkout": false, "py_versions": ["all"] From 2fc1dfca96c5fbbb7c2e13df93d1cde1ea74a40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 21 Aug 2020 15:42:58 +0200 Subject: [PATCH 029/680] Require Sphinx 3 --- Pipfile | 4 ++-- Pipfile.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Pipfile b/Pipfile index feefb857132..18e8a545617 100644 --- a/Pipfile +++ b/Pipfile @@ -4,13 +4,13 @@ url = "https://pypi.python.org/simple" verify_ssl = true [dev-packages] -Sphinx = "*" +Sphinx = ">=3.1.2" coverage = "*" docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 flake8 = "*" flake8-bugbear = "*" flake8-mypy = "*" -mypy = ">=0.740" +mypy = ">=0.782" pre-commit = "*" readme_renderer = "*" recommonmark = "*" diff --git a/Pipfile.lock b/Pipfile.lock index b8dfd6b5422..ddbf9b99520 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4a6956c7c81b496d3fd7a4e3395b332d4dc9a5bed468e36e729a4039c739ad2d" + "sha256": "682054eb4a3d4366e2f76b3ae74286d156a270c0d7b57299a81f8cc1d0a51d19" }, "pipfile-spec": 6, "requires": {}, @@ -58,11 +58,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "black": { "editable": true, @@ -187,9 +187,9 @@ "toml": { "hashes": [ "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" ], "index": "pypi", "version": "==0.10.1" @@ -305,11 +305,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "babel": { "hashes": [ @@ -848,9 +848,9 @@ "toml": { "hashes": [ "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" ], "index": "pypi", "version": "==0.10.1" From cb6f2198b8fc700f8cc0d36c338d613e509250f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 21 Aug 2020 15:46:00 +0200 Subject: [PATCH 030/680] Use properly renamed function name in docs --- docs/reference/reference_functions.rst | 2 +- src/black/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/reference_functions.rst b/docs/reference/reference_functions.rst index 1beecc10325..a7184115c94 100644 --- a/docs/reference/reference_functions.rst +++ b/docs/reference/reference_functions.rst @@ -171,7 +171,7 @@ Utilities .. autofunction:: black.re_compile_maybe_verbose -.. autofunction:: black.should_explode +.. autofunction:: black.should_split_body_explode .. autofunction:: black.shutdown diff --git a/src/black/__init__.py b/src/black/__init__.py index 8d0c70f06c7..faa88b38af7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5559,7 +5559,7 @@ def ensure_visible(leaf: Leaf) -> None: def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: - """Should `line` immediately be split with `delimiter_split()` after RHS?""" + """Should `line` be immediately split with `delimiter_split()` after RHS?""" if not (opening_bracket.parent and opening_bracket.value in "[{("): return False From 205f3b67fbddc7ba0fa330c0493821e404451b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 21 Aug 2020 15:55:03 +0200 Subject: [PATCH 031/680] Fix dealing with generated files in docs --- docs/authors.md | 183 +++++++++++ docs/change_log.md | 467 ++++++++++++++++++++++++++++ docs/conf.py | 8 +- docs/contributing_to_black.md | 70 +++++ docs/github_actions.md | 19 ++ docs/ignoring_unmodified_files.md | 23 ++ docs/installation_and_usage.md | 179 +++++++++++ docs/pyproject_toml.md | 88 ++++++ docs/show_your_style.md | 19 ++ docs/version_control_integration.md | 28 ++ 10 files changed, 1079 insertions(+), 5 deletions(-) create mode 100644 docs/authors.md create mode 100644 docs/change_log.md create mode 100644 docs/contributing_to_black.md create mode 100644 docs/github_actions.md create mode 100644 docs/ignoring_unmodified_files.md create mode 100644 docs/installation_and_usage.md create mode 100644 docs/pyproject_toml.md create mode 100644 docs/show_your_style.md create mode 100644 docs/version_control_integration.md diff --git a/docs/authors.md b/docs/authors.md new file mode 100644 index 00000000000..6a3a8d63f91 --- /dev/null +++ b/docs/authors.md @@ -0,0 +1,183 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Authors + +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), and +[Cooper Lees](mailto:me@cooperlees.com). + +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) +- [Alex Vandiver](mailto:github@chmrr.net) +- [Allan Simon](mailto:allan.simon@supinfo.com) +- Anders-Petter Ljungquist +- [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) +- [Andrew Zhou](mailto:andrewfzhou@gmail.com) +- [Andrey](mailto:dyuuus@yandex.ru) +- [Andy Freeland](mailto:andy@andyfreeland.net) +- [Anthony Sottile](mailto:asottile@umich.edu) +- [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) +- [Arnav Borbornah](mailto:arnavborborah11@gmail.com) +- [Artem Malyshev](mailto:proofit404@gmail.com) +- [Asger Hautop Drewsen](mailto:asgerdrewsen@gmail.com) +- [Augie Fackler](mailto:raf@durin42.com) +- [Aviskar KC](mailto:aviskarkc10@gmail.com) +- Batuhan Taşkaya +- [Benjamin Wohlwend](mailto:bw@piquadrat.ch) +- [Benjamin Woodruff](mailto:github@benjam.info) +- [Bharat Raghunathan](mailto:bharatraghunthan9767@gmail.com) +- [Brandt Bucher](mailto:brandtbucher@gmail.com) +- [Brett Cannon](mailto:brett@python.org) +- [Bryan Bugyi](mailto:bryan.bugyi@rutgers.edu) +- [Bryan Forbes](mailto:bryan@reigndropsfall.net) +- [Calum Lind](mailto:calumlind@gmail.com) +- [Charles](mailto:peacech@gmail.com) +- Charles Reid +- [Christian Clauss](mailto:cclauss@bluewin.ch) +- [Christian Heimes](mailto:christian@python.org) +- [Chuck Wooters](mailto:chuck.wooters@microsoft.com) +- [Chris Rose](mailto:offline@offby1.net) +- Codey Oxley +- [Cong](mailto:congusbongus@gmail.com) +- [Cooper Ry Lees](mailto:me@cooperlees.com) +- [Dan Davison](mailto:dandavison7@gmail.com) +- [Daniel Hahler](mailto:github@thequod.de) +- [Daniel M. Capella](mailto:polycitizen@gmail.com) +- Daniele Esposti +- [David Hotham](mailto:david.hotham@metaswitch.com) +- [David Lukes](mailto:dafydd.lukes@gmail.com) +- [David Szotten](mailto:davidszotten@gmail.com) +- [Denis Laxalde](mailto:denis@laxalde.org) +- [Douglas Thor](mailto:dthor@transphormusa.com) +- dylanjblack +- [Eli Treuherz](mailto:eli@treuherz.com) +- [Emil Hessman](mailto:emil@hessman.se) +- [Felix Kohlgrüber](mailto:felix.kohlgrueber@gmail.com) +- [Florent Thiery](mailto:fthiery@gmail.com) +- Francisco +- [Giacomo Tagliabue](mailto:giacomo.tag@gmail.com) +- [Greg Gandenberger](mailto:ggandenberger@shoprunner.com) +- [Gregory P. Smith](mailto:greg@krypto.org) +- Gustavo Camargo +- hauntsaninja +- [Heaford](mailto:dan@heaford.com) +- [Hugo Barrera](mailto::hugo@barrera.io) +- Hugo van Kemenade +- [Hynek Schlawack](mailto:hs@ox.cx) +- [Ivan Katanić](mailto:ivan.katanic@gmail.com) +- [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) +- [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) +- [Jan Hnátek](mailto:jan.hnatek@gmail.com) +- [Jason Fried](mailto:me@jasonfried.info) +- [Jason Friedland](mailto:jason@friedland.id.au) +- [jgirardet](mailto:ijkl@netc.fr) +- Jim Brännlund +- [Jimmy Jia](mailto:tesrin@gmail.com) +- [Joe Antonakakis](mailto:jma353@cornell.edu) +- [Jon Dufresne](mailto:jon.dufresne@gmail.com) +- [Jonas Obrist](mailto:ojiidotch@gmail.com) +- [Jonty Wareing](mailto:jonty@jonty.co.uk) +- [Jose Nazario](mailto:jose.monkey.org@gmail.com) +- [Joseph Larson](mailto:larson.joseph@gmail.com) +- [Josh Bode](mailto:joshbode@fastmail.com) +- [Josh Holland](mailto:anowlcalledjosh@gmail.com) +- [José Padilla](mailto:jpadilla@webapplicate.com) +- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) +- [kaiix](mailto:kvn.hou@gmail.com) +- [Katie McLaughlin](mailto:katie@glasnt.com) +- Katrin Leinweber +- [Keith Smiley](mailto:keithbsmiley@gmail.com) +- [Kenyon Ralph](mailto:kenyon@kenyonralph.com) +- [Kevin Kirsche](mailto:Kev.Kirsche+GitHub@gmail.com) +- [Kyle Hausmann](mailto:kyle.hausmann@gmail.com) +- [Kyle Sunden](mailto:sunden@wisc.edu) +- Lawrence Chan +- [Linus Groh](mailto:mail@linusgroh.de) +- [Loren Carvalho](mailto:comradeloren@gmail.com) +- [Luka Sterbic](mailto:luka.sterbic@gmail.com) +- [LukasDrude](mailto:mail@lukas-drude.de) +- Mahmoud Hossam +- Mariatta +- [Matt VanEseltine](mailto:vaneseltine@gmail.com) +- [Matthew Clapp](mailto:itsayellow+dev@gmail.com) +- [Matthew Walster](mailto:matthew@walster.org) +- Max Smolens +- [Michael Aquilina](mailto:michaelaquilina@gmail.com) +- [Michael Flaxman](mailto:michael.flaxman@gmail.com) +- [Michael J. Sullivan](mailto:sully@msully.net) +- [Michael McClimon](mailto:michael@mcclimon.org) +- [Miguel Gaiowski](mailto:miggaiowski@gmail.com) +- [Mike](mailto:roshi@fedoraproject.org) +- [mikehoyio](mailto:mikehoy@gmail.com) +- [Min ho Kim](mailto:minho42@gmail.com) +- [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) +- MomIsBestFriend +- [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) +- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) +- [Neraste](mailto:neraste.herr10@gmail.com) +- [Nikolaus Waxweiler](mailto:madigens@gmail.com) +- [Ofek Lev](mailto:ofekmeister@gmail.com) +- [Osaetin Daniel](mailto:osaetindaniel@gmail.com) +- [otstrel](mailto:otstrel@gmail.com) +- [Pablo Galindo](mailto:Pablogsal@gmail.com) +- [Paul Ganssle](mailto:p.ganssle@gmail.com) +- [Paul Meinhardt](mailto:mnhrdt@gmail.com) +- [Peter Bengtsson](mailto:mail@peterbe.com) +- [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) +- pmacosta +- [Quentin Pradet](mailto:quentin@pradet.me) +- [Ralf Schmitt](mailto:ralf@systemexit.de) +- [Ramón Valles](mailto:mroutis@protonmail.com) +- [Richard Fearn](mailto:richardfearn@gmail.com) +- Richard Si +- [Rishikesh Jha](mailto:rishijha424@gmail.com) +- [Rupert Bedford](mailto:rupert@rupertb.com) +- Russell Davis +- [Rémi Verschelde](mailto:rverschelde@gmail.com) +- [Sami Salonen](mailto:sakki@iki.fi) +- [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) +- [Sanket Dasgupta](mailto:sanketdasgupta@gmail.com) +- Sergi +- [Scott Stevenson](mailto:scott@stevenson.io) +- Shantanu +- [shaoran](mailto:shaoran@sakuranohana.org) +- [Shinya Fujino](mailto:shf0811@gmail.com) +- springstan +- [Stavros Korokithakis](mailto:hi@stavros.io) +- [Stephen Rosen](mailto:sirosen@globus.org) +- [Steven M. Vascellaro](mailto:S.Vascellaro@gmail.com) +- [Sunil Kapil](mailto:snlkapil@gmail.com) +- [Sébastien Eustace](mailto:sebastien.eustace@gmail.com) +- [Tal Amuyal](mailto:TalAmuyal@gmail.com) +- [Terrance](mailto:git@terrance.allofti.me) +- [Thom Lu](mailto:thomas.c.lu@gmail.com) +- [Thomas Grainger](mailto:tagrain@gmail.com) +- [Tim Gates](mailto:tim.gates@iress.com) +- [Tim Swast](mailto:swast@google.com) +- [Timo](mailto:timo_tk@hotmail.com) +- Toby Fleming +- [Tom Christie](mailto:tom@tomchristie.com) +- [Tony Narlock](mailto:tony@git-pull.com) +- [Tsuyoshi Hombashi](mailto:tsuyoshi.hombashi@gmail.com) +- [Tushar Chandra](mailto:tusharchandra2018@u.northwestern.edu) +- [Tzu-ping Chung](mailto:uranusjr@gmail.com) +- [Utsav Shah](mailto:ukshah2@illinois.edu) +- utsav-dbx +- vezeli +- [Ville Skyttä](mailto:ville.skytta@iki.fi) +- [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com) +- [Vlad Emelianov](mailto:volshebnyi@gmail.com) +- [williamfzc](mailto:178894043@qq.com) +- [wouter bolsterlee](mailto:wouter@bolsterl.ee) +- Yazdan +- [Yngve Høiseth](mailto:yngve@hoiseth.net) +- [Yurii Karabas](mailto:1998uriyyo@gmail.com) diff --git a/docs/change_log.md b/docs/change_log.md new file mode 100644 index 00000000000..3cc8c40d8c6 --- /dev/null +++ b/docs/change_log.md @@ -0,0 +1,467 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CHANGES.md" + +## Change Log + +### Unreleased + +#### _Black_ + +- re-implemented support for explicit trailing commas: now it works consistently within + any bracket pair, including nested structures (#1288 and duplicates) + +- reindent docstrings when reindenting code around it (#1053) + +- show colored diffs (#1266) + +- move to 'py3' tagged wheels (#1388) + +- remove deprecated `--py36` option (#1236) + +- add `--force-exclude` argument (#1032) + +#### Vim plugin + +- prefer virtualenv packages over global packages (#1383) + +### 19.10b0 + +- added support for PEP 572 assignment expressions (#711) + +- added support for PEP 570 positional-only arguments (#943) + +- added support for async generators (#593) + +- added support for pre-splitting collections by putting an explicit trailing comma + inside (#826) + +- added `black -c` as a way to format code passed from the command line (#761) + +- --safe now works with Python 2 code (#840) + +- fixed grammar selection for Python 2-specific code (#765) + +- fixed feature detection for trailing commas in function definitions and call sites + (#763) + +- `# fmt: off`/`# fmt: on` comment pairs placed multiple times within the same block of + code now behave correctly (#1005) + +- _Black_ no longer crashes on Windows machines with more than 61 cores (#838) + +- _Black_ no longer crashes on standalone comments prepended with a backslash (#767) + +- _Black_ no longer crashes on `from` ... `import` blocks with comments (#829) + +- _Black_ no longer crashes on Python 3.7 on some platform configurations (#494) + +- _Black_ no longer fails on comments in from-imports (#671) + +- _Black_ no longer fails when the file starts with a backslash (#922) + +- _Black_ no longer merges regular comments with type comments (#1027) + +- _Black_ no longer splits long lines that contain type comments (#997) + +- removed unnecessary parentheses around `yield` expressions (#834) + +- added parentheses around long tuples in unpacking assignments (#832) + +- added parentheses around complex powers when they are prefixed by a unary operator + (#646) + +- fixed bug that led _Black_ format some code with a line length target of 1 (#762) + +- _Black_ no longer introduces quotes in f-string subexpressions on string boundaries + (#863) + +- if _Black_ puts parenthesis around a single expression, it moves comments to the + wrapped expression instead of after the brackets (#872) + +- `blackd` now returns the version of _Black_ in the response headers (#1013) + +- `blackd` can now output the diff of formats on source code when the `X-Diff` header is + provided (#969) + +### 19.3b0 + +- new option `--target-version` to control which Python versions _Black_-formatted code + should target (#618) + +- deprecated `--py36` (use `--target-version=py36` instead) (#724) + +- _Black_ no longer normalizes numeric literals to include `_` separators (#696) + +- long `del` statements are now split into multiple lines (#698) + +- type comments are no longer mangled in function signatures + +- improved performance of formatting deeply nested data structures (#509) + +- _Black_ now properly formats multiple files in parallel on Windows (#632) + +- _Black_ now creates cache files atomically which allows it to be used in parallel + pipelines (like `xargs -P8`) (#673) + +- _Black_ now correctly indents comments in files that were previously formatted with + tabs (#262) + +- `blackd` now supports CORS (#622) + +### 18.9b0 + +- numeric literals are now formatted by _Black_ (#452, #461, #464, #469): + + - numeric literals are normalized to include `_` separators on Python 3.6+ code + + - added `--skip-numeric-underscore-normalization` to disable the above behavior and + leave numeric underscores as they were in the input + + - code with `_` in numeric literals is recognized as Python 3.6+ + + - most letters in numeric literals are lowercased (e.g., in `1e10`, `0x01`) + + - hexadecimal digits are always uppercased (e.g. `0xBADC0DE`) + +- added `blackd`, see [its documentation](#blackd) for more info (#349) + +- adjacent string literals are now correctly split into multiple lines (#463) + +- trailing comma is now added to single imports that don't fit on a line (#250) + +- cache is now populated when `--check` is successful for a file which speeds up + consecutive checks of properly formatted unmodified files (#448) + +- whitespace at the beginning of the file is now removed (#399) + +- fixed mangling [pweave](http://mpastell.com/pweave/) and + [Spyder IDE](https://www.spyder-ide.org/) special comments (#532) + +- fixed unstable formatting when unpacking big tuples (#267) + +- fixed parsing of `__future__` imports with renames (#389) + +- fixed scope of `# fmt: off` when directly preceding `yield` and other nodes (#385) + +- fixed formatting of lambda expressions with default arguments (#468) + +- fixed `async for` statements: _Black_ no longer breaks them into separate lines (#372) + +- note: the Vim plugin stopped registering `,=` as a default chord as it turned out to + be a bad idea (#415) + +### 18.6b4 + +- hotfix: don't freeze when multiple comments directly precede `# fmt: off` (#371) + +### 18.6b3 + +- typing stub files (`.pyi`) now have blank lines added after constants (#340) + +- `# fmt: off` and `# fmt: on` are now much more dependable: + + - they now work also within bracket pairs (#329) + + - they now correctly work across function/class boundaries (#335) + + - they now work when an indentation block starts with empty lines or misaligned + comments (#334) + +- made Click not fail on invalid environments; note that Click is right but the + likelihood we'll need to access non-ASCII file paths when dealing with Python source + code is low (#277) + +- fixed improper formatting of f-strings with quotes inside interpolated expressions + (#322) + +- fixed unnecessary slowdown when long list literals where found in a file + +- fixed unnecessary slowdown on AST nodes with very many siblings + +- fixed cannibalizing backslashes during string normalization + +- fixed a crash due to symbolic links pointing outside of the project directory (#338) + +### 18.6b2 + +- added `--config` (#65) + +- added `-h` equivalent to `--help` (#316) + +- fixed improper unmodified file caching when `-S` was used + +- fixed extra space in string unpacking (#305) + +- fixed formatting of empty triple quoted strings (#313) + +- fixed unnecessary slowdown in comment placement calculation on lines without comments + +### 18.6b1 + +- hotfix: don't output human-facing information on stdout (#299) + +- hotfix: don't output cake emoji on non-zero return code (#300) + +### 18.6b0 + +- added `--include` and `--exclude` (#270) + +- added `--skip-string-normalization` (#118) + +- added `--verbose` (#283) + +- the header output in `--diff` now actually conforms to the unified diff spec + +- fixed long trivial assignments being wrapped in unnecessary parentheses (#273) + +- fixed unnecessary parentheses when a line contained multiline strings (#232) + +- fixed stdin handling not working correctly if an old version of Click was used (#276) + +- _Black_ now preserves line endings when formatting a file in place (#258) + +### 18.5b1 + +- added `--pyi` (#249) + +- added `--py36` (#249) + +- Python grammar pickle caches are stored with the formatting caches, making _Black_ + work in environments where site-packages is not user-writable (#192) + +- _Black_ now enforces a PEP 257 empty line after a class-level docstring (and/or + fields) and the first method + +- fixed invalid code produced when standalone comments were present in a trailer that + was omitted from line splitting on a large expression (#237) + +- fixed optional parentheses being removed within `# fmt: off` sections (#224) + +- fixed invalid code produced when stars in very long imports were incorrectly wrapped + in optional parentheses (#234) + +- fixed unstable formatting when inline comments were moved around in a trailer that was + omitted from line splitting on a large expression (#238) + +- fixed extra empty line between a class declaration and the first method if no class + docstring or fields are present (#219) + +- fixed extra empty line between a function signature and an inner function or inner + class (#196) + +### 18.5b0 + +- call chains are now formatted according to the + [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67) + +- data structure literals (tuples, lists, dictionaries, and sets) are now also always + exploded like imports when they don't fit in a single line (#152) + +- slices are now formatted according to PEP 8 (#178) + +- parentheses are now also managed automatically on the right-hand side of assignments + and return statements (#140) + +- math operators now use their respective priorities for delimiting multiline + expressions (#148) + +- optional parentheses are now omitted on expressions that start or end with a bracket + and only contain a single operator (#177) + +- empty parentheses in a class definition are now removed (#145, #180) + +- string prefixes are now standardized to lowercase and `u` is removed on Python 3.6+ + only code and Python 2.7+ code with the `unicode_literals` future import (#188, #198, + #199) + +- typing stub files (`.pyi`) are now formatted in a style that is consistent with PEP + 484 (#207, #210) + +- progress when reformatting many files is now reported incrementally + +- fixed trailers (content with brackets) being unnecessarily exploded into their own + lines after a dedented closing bracket (#119) + +- fixed an invalid trailing comma sometimes left in imports (#185) + +- fixed non-deterministic formatting when multiple pairs of removable parentheses were + used (#183) + +- fixed multiline strings being unnecessarily wrapped in optional parentheses in long + assignments (#215) + +- fixed not splitting long from-imports with only a single name + +- fixed Python 3.6+ file discovery by also looking at function calls with unpacking. + This fixed non-deterministic formatting if trailing commas where used both in function + signatures with stars and function calls with stars but the former would be + reformatted to a single line. + +- fixed crash on dealing with optional parentheses (#193) + +- fixed "is", "is not", "in", and "not in" not considered operators for splitting + purposes + +- fixed crash when dead symlinks where encountered + +### 18.4a4 + +- don't populate the cache on `--check` (#175) + +### 18.4a3 + +- added a "cache"; files already reformatted that haven't changed on disk won't be + reformatted again (#109) + +- `--check` and `--diff` are no longer mutually exclusive (#149) + +- generalized star expression handling, including double stars; this fixes + multiplication making expressions "unsafe" for trailing commas (#132) + +- _Black_ no longer enforces putting empty lines behind control flow statements (#90) + +- _Black_ now splits imports like "Mode 3 + trailing comma" of isort (#127) + +- fixed comment indentation when a standalone comment closes a block (#16, #32) + +- fixed standalone comments receiving extra empty lines if immediately preceding a + class, def, or decorator (#56, #154) + +- fixed `--diff` not showing entire path (#130) + +- fixed parsing of complex expressions after star and double stars in function calls + (#2) + +- fixed invalid splitting on comma in lambda arguments (#133) + +- fixed missing splits of ternary expressions (#141) + +### 18.4a2 + +- fixed parsing of unaligned standalone comments (#99, #112) + +- fixed placement of dictionary unpacking inside dictionary literals (#111) + +- Vim plugin now works on Windows, too + +- fixed unstable formatting when encountering unnecessarily escaped quotes in a string + (#120) + +### 18.4a1 + +- added `--quiet` (#78) + +- added automatic parentheses management (#4) + +- added [pre-commit](https://pre-commit.com) integration (#103, #104) + +- fixed reporting on `--check` with multiple files (#101, #102) + +- fixed removing backslash escapes from raw strings (#100, #105) + +### 18.4a0 + +- added `--diff` (#87) + +- add line breaks before all delimiters, except in cases like commas, to better comply + with PEP 8 (#73) + +- standardize string literals to use double quotes (almost) everywhere (#75) + +- fixed handling of standalone comments within nested bracketed expressions; _Black_ + will no longer produce super long lines or put all standalone comments at the end of + the expression (#22) + +- fixed 18.3a4 regression: don't crash and burn on empty lines with trailing whitespace + (#80) + +- fixed 18.3a4 regression: `# yapf: disable` usage as trailing comment would cause + _Black_ to not emit the rest of the file (#95) + +- when CTRL+C is pressed while formatting many files, _Black_ no longer freaks out with + a flurry of asyncio-related exceptions + +- only allow up to two empty lines on module level and only single empty lines within + functions (#74) + +### 18.3a4 + +- `# fmt: off` and `# fmt: on` are implemented (#5) + +- automatic detection of deprecated Python 2 forms of print statements and exec + statements in the formatted file (#49) + +- use proper spaces for complex expressions in default values of typed function + arguments (#60) + +- only return exit code 1 when --check is used (#50) + +- don't remove single trailing commas from square bracket indexing (#59) + +- don't omit whitespace if the previous factor leaf wasn't a math operator (#55) + +- omit extra space in kwarg unpacking if it's the first argument (#46) + +- omit extra space in + [Sphinx auto-attribute comments](http://www.sphinx-doc.org/en/stable/ext/autodoc.html#directive-autoattribute) + (#68) + +### 18.3a3 + +- don't remove single empty lines outside of bracketed expressions (#19) + +- added ability to pipe formatting from stdin to stdin (#25) + +- restored ability to format code with legacy usage of `async` as a name (#20, #42) + +- even better handling of numpy-style array indexing (#33, again) + +### 18.3a2 + +- changed positioning of binary operators to occur at beginning of lines instead of at + the end, following + [a recent change to PEP 8](https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b) + (#21) + +- ignore empty bracket pairs while splitting. This avoids very weirdly looking + formattings (#34, #35) + +- remove a trailing comma if there is a single argument to a call + +- if top level functions were separated by a comment, don't put four empty lines after + the upper function + +- fixed unstable formatting of newlines with imports + +- fixed unintentional folding of post scriptum standalone comments into last statement + if it was a simple statement (#18, #28) + +- fixed missing space in numpy-style array indexing (#33) + +- fixed spurious space after star-based unary expressions (#31) + +### 18.3a1 + +- added `--check` + +- only put trailing commas in function signatures and calls if it's safe to do so. If + the file is Python 3.6+ it's always safe, otherwise only safe if there are no `*args` + or `**kwargs` used in the signature or call. (#8) + +- fixed invalid spacing of dots in relative imports (#6, #13) + +- fixed invalid splitting after comma on unpacked variables in for-loops (#23) + +- fixed spurious space in parenthesized set expressions (#7) + +- fixed spurious space after opening parentheses and in default arguments (#14, #17) + +- fixed spurious space after unary operators when the operand was a complex expression + (#15) + +### 18.3a0 + +- first published version, Happy 🍰 Day 2018! + +- alpha quality + +- date-versioned (see: https://calver.org/) diff --git a/docs/conf.py b/docs/conf.py index 9e6a9bcaa62..8dd77b41d61 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -184,11 +184,9 @@ def process_sections( contents = fix_headers(contents) with open(target_path, "w", encoding="utf-8") as f: - if section.src.suffix == ".md": - f.write( - "[//]: # (NOTE: THIS FILE WAS AUTOGENERATED FROM" - f" {section.src})\n\n" - ) + if section.src.suffix == ".md" and section.src != target_path: + rel = section.src.resolve().relative_to(CURRENT_DIR.parent) + f.write(f'[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM {rel}"\n\n') f.write(contents) processed_sections.add(section.name) modified_files.add(target_path) diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md new file mode 100644 index 00000000000..e5307adb5d0 --- /dev/null +++ b/docs/contributing_to_black.md @@ -0,0 +1,70 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CONTRIBUTING.md" + +# Contributing to _Black_ + +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? + +## Bird's eye view + +In terms of inspiration, _Black_ is about as configurable as _gofmt_. This is +deliberate. + +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. + +## Technicalities + +Development on the latest version of Python is preferred. As of this writing it's 3.8. +You can use any operating system. I am using macOS myself and CentOS at work. + +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. + +Before submitting pull requests, run lints and tests with: + +```console +$ pre-commit run -a +$ python -m unittest +$ black-primer [-k -w /tmp/black_test_repos] +``` + +## 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/master/src/black_primer/primer.json) +configuration for it to pass. + +For more `black-primer` information visit the +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). + +## Hygiene + +If you're fixing a bug, add a test. Run it first to confirm it fails, then fix the bug, +run it again to confirm it's really fixed. + +If adding a new feature, add a test. In fact, always add a test. But wait, before adding +any large feature, first open an issue for us to discuss the idea first. + +## Finally + +Thanks again for your interest in improving the project! You're taking action when most +people decide to sit and watch. diff --git a/docs/github_actions.md b/docs/github_actions.md new file mode 100644 index 00000000000..7ff87540242 --- /dev/null +++ b/docs/github_actions.md @@ -0,0 +1,19 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# GitHub Actions + +Create a file named `.github/workflows/black.yml` inside your repository with: + +```yaml +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable +``` diff --git a/docs/ignoring_unmodified_files.md b/docs/ignoring_unmodified_files.md new file mode 100644 index 00000000000..a915f4e8678 --- /dev/null +++ b/docs/ignoring_unmodified_files.md @@ -0,0 +1,23 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Ignoring unmodified files + +_Black_ remembers files it has already formatted, unless the `--diff` flag is used or +code is passed via standard input. This information is stored per-user. The exact +location of the file depends on the _Black_ version and the system on which _Black_ is +run. The file is non-portable. The standard location on common operating systems is: + +- Windows: + `C:\\Users\\AppData\Local\black\black\Cache\\cache...pickle` +- macOS: + `/Users//Library/Caches/black//cache...pickle` +- Linux: + `/home//.cache/black//cache...pickle` + +`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//`. diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md new file mode 100644 index 00000000000..cc0269198a2 --- /dev/null +++ b/docs/installation_and_usage.md @@ -0,0 +1,179 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Installation and usage + +## Installation + +_Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to +run but you can reformat Python 2 code with it, too. + +### Install from GitHub + +If you can't wait for the latest _hotness_ and want to install from GitHub, use: + +`pip install git+git://github.com/psf/black` + +## Usage + +To get started right away with sensible defaults: + +```sh +black {source_file_or_directory} +``` + +You can run _Black_ as a package if running it as a script doesn't work: + +```sh +python -m black {source_file_or_directory} +``` + +## Command line options + +_Black_ doesn't provide many options. You can list them by running `black --help`: + +```text +Usage: black [OPTIONS] [SRC]... + + The uncompromising code formatter. + +Options: + -c, --code TEXT Format the code passed in as a string. + -l, --line-length INTEGER How many characters per line to allow. + [default: 88] + + -t, --target-version [py27|py33|py34|py35|py36|py37|py38] + Python versions that should be supported by + Black's output. [default: per-file auto- + detection] + + --pyi Format all input files like typing stubs + regardless of file extension (useful when + piping source on standard input). + + -S, --skip-string-normalization + Don't normalize string quotes or prefixes. + --check Don't write the files back, just return the + status. Return code 0 means nothing would + change. Return code 1 means some files + would be reformatted. Return code 123 means + there was an internal error. + + --diff Don't write the files back, just output a + diff for each file on stdout. + + --color / --no-color Show colored diff. Only applies when + `--diff` is given. + + --fast / --safe If --fast given, skip temporary sanity + checks. [default: --safe] + + --include TEXT 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. + [default: \.pyi?$] + + --exclude TEXT 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: /(\.eggs|\.git|\.hg|\.mypy + _cache|\.nox|\.tox|\.venv|\.svn|_build|buck- + out|build|dist)/] + + --force-exclude TEXT Like --exclude, but files and directories + matching this regex will be excluded even + when they are passed explicitly as arguments + + -q, --quiet Don't emit non-error messages to stderr. + Errors are still emitted; silence those with + 2>/dev/null. + + -v, --verbose Also emit messages to stderr about files + that were not changed or were ignored due to + --exclude=. + + --version Show the version and exit. + --config FILE Read configuration from FILE path. + -h, --help Show this message and exit. +``` + +_Black_ is a well-behaved Unix-style command-line tool: + +- it does nothing if no sources are passed to it; +- 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). + +## Using _Black_ with other tools + +While _Black_ enforces formatting that conforms to PEP 8, other tools may raise warnings +about _Black_'s changes or will overwrite _Black_'s changes. A good example of this is +[isort](https://pypi.org/p/isort). Since _Black_ is barely configurable, these tools +should be configured to neither warn about nor overwrite _Black_'s changes. + +Actual details on _Black_ compatible configurations for various tools can be found in +[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md). + +## Migrating your code style without ruining git blame + +A long-standing argument against moving to automated code formatters like _Black_ is +that the migration will clutter up the output of `git blame`. This was a valid argument, +but since Git version 2.23, Git natively supports +[ignoring revisions in blame](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt) +with the `--ignore-rev` option. You can also pass a file listing the revisions to ignore +using the `--ignore-revs-file` option. The changes made by the revision will be ignored +when assigning blame. Lines modified by an ignored revision will be blamed on the +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. + +``` +# Migrate code style to Black +5b4ab991dede475d393e9d69ec388fd6bd949699 +``` + +Afterwards, you can pass that file to `git blame` and see clean and meaningful blame +information. + +```console +$ git blame important.py --ignore-revs-file .git-blame-ignore-revs +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file): +abdfd8b0 (Alice Doe 2019-09-23 11:39:32 -0400 2) text = text.lstrip() +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3) with open(file, "r+") as f: +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4) f.write(formatted) +``` + +You can even configure `git` to automatically ignore revisions listed in a file on every +call to `git blame`. + +```console +$ 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!) + +## NOTE: This is a beta product + +_Black_ is already [successfully used](#used-by) by many projects, small and big. It +also sports a decent test suite. However, it is still very new. 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. + +Also, as a temporary safety measure, _Black_ will check that the reformatted code still +produces a valid AST that is equivalent to the original. This slows it down. If you're +feeling confident, use `--fast`. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md new file mode 100644 index 00000000000..cd313452b1e --- /dev/null +++ b/docs/pyproject_toml.md @@ -0,0 +1,88 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# pyproject.toml + +_Black_ is able to read project-specific default values for its command line options +from a `pyproject.toml` file. This is especially useful for specifying custom +`--include` and `--exclude` patterns for your project. + +**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is +"No". _Black_ is all about sensible defaults. + +## What on Earth is a `pyproject.toml` file? + +[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. + +## Where _Black_ looks for the file + +By default _Black_ looks for `pyproject.toml` starting from the common base directory of +all files and directories passed on the command line. If it's not there, it looks in +parent directories. It stops looking when it finds the file, or a `.git` directory, or a +`.hg` directory, or the root of the file system, whichever comes first. + +If you're formatting standard input, _Black_ will look for configuration starting from +the current working directory. + +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. + +Please note `blackd` will not use `pyproject.toml` configuration. + +## Configuration format + +As the file extension suggests, `pyproject.toml` is a +[TOML](https://github.com/toml-lang/toml) file. It contains separate sections for +different tools. _Black_ is using the `[tool.black]` section. The option keys are the +same as long names of options on the command line. + +Note that you have to use single-quoted strings in TOML for regular expressions. It's +the equivalent of r-strings in Python. Multiline strings are treated as verbose regular +expressions by Black. Use `[ ]` to denote a significant space character. + +
+Example pyproject.toml + +```toml +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + | foo.py # also separately exclude a file named foo.py in + # the root of the project +) +''' +``` + +
+ +## Lookup hierarchy + +Command-line options have defaults that you can see in `--help`. A `pyproject.toml` can +override those defaults. Finally, options provided by the user on the command line +override both. + +_Black_ will only ever use one `pyproject.toml` file during an entire run. It doesn't +look for multiple files, and doesn't compose configuration from different levels of the +file hierarchy. diff --git a/docs/show_your_style.md b/docs/show_your_style.md new file mode 100644 index 00000000000..67b213c3965 --- /dev/null +++ b/docs/show_your_style.md @@ -0,0 +1,19 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# 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: + +``` +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +``` + +Looks like this: +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md new file mode 100644 index 00000000000..25dac308c47 --- /dev/null +++ b/docs/version_control_integration.md @@ -0,0 +1,28 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Version control integration + +Use [pre-commit](https://pre-commit.com/). Once you +[have it installed](https://pre-commit.com/#install), add this to the +`.pre-commit-config.yaml` in your repository: + +```yaml +repos: + - repo: https://github.com/psf/black + rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags + hooks: + - id: black + language_version: python3 # Should be a command that runs python3.6+ +``` + +Then run `pre-commit install` and you're ready to go. + +Avoid using `args` in the hook. Instead, store necessary configuration in +`pyproject.toml` so that editors and command-line usage of Black all behave consistently +for your project. See _Black_'s own +[pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an +example. + +If you're already using Python 3.7, switch the `language_version` accordingly. Finally, +`stable` is a branch that tracks the latest release on PyPI. If you'd rather run on +master, this is also an option. From cd3a93a14689f046468ece2a5b1f78863c3c4cd2 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Fri, 21 Aug 2020 20:58:58 +1000 Subject: [PATCH 032/680] Property-based fuzz test --- .github/workflows/fuzz.yml | 31 ++++++++++++++++++++ .gitignore | 1 + README.md | 1 + fuzz.py | 59 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 .github/workflows/fuzz.yml create mode 100644 fuzz.py diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000000..92caa0fd5c1 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,31 @@ +name: Fuzz + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8] + + 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 --upgrade coverage + python -m pip install --upgrade hypothesmith + python -m pip install -e ".[d]" + + - name: Run fuzz tests + run: | + coverage run fuzz.py + coverage report diff --git a/.gitignore b/.gitignore index 509797e65c4..6b94cacd183 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ src/_black_version.py .eggs .dmypy.json *.swp +.hypothesis/ diff --git a/README.md b/README.md index 44f2d207c8b..20f6fa420b2 100644 --- a/README.md +++ b/README.md @@ -684,3 +684,4 @@ Multiple contributions by: - Yazdan - [Yngve Høiseth](mailto:yngve@hoiseth.net) - [Yurii Karabas](mailto:1998uriyyo@gmail.com) +- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/fuzz.py b/fuzz.py new file mode 100644 index 00000000000..fdd4917f2ec --- /dev/null +++ b/fuzz.py @@ -0,0 +1,59 @@ +"""Property-based tests for Black. + +By Zac Hatfield-Dodds, based on my Hypothesmith tool for source code +generation. You can run this file with `python`, `pytest`, or (soon) +a coverage-guided fuzzer I'm working on. +""" + +import hypothesmith +from hypothesis import HealthCheck, given, settings, strategies as st + +import black + + +# This test uses the Hypothesis and Hypothesmith libraries to generate random +# syntatically-valid Python source code and run Black in odd modes. +@settings( + 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. +) +@given( + # Note that while Hypothesmith might generate code unlike that written by + # humans, it's a general test that should pass for any *valid* source code. + # (so e.g. running it against code scraped of the internet might also help) + src_contents=hypothesmith.from_grammar() | hypothesmith.from_node(), + # Using randomly-varied modes helps us to exercise less common code paths. + mode=st.builds( + black.FileMode, + line_length=st.just(88) | st.integers(0, 200), + string_normalization=st.booleans(), + is_pyi=st.booleans(), + ), +) +def test_idempotent_any_syntatically_valid_python( + src_contents: str, mode: black.FileMode +) -> None: + # Before starting, let's confirm that the input string is valid Python: + compile(src_contents, "", "exec") # else the bug is in hypothesmith + + # Then format the code... + try: + 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 code with it. See issues #970, #1012, #1358, and #1557. + # TODO: remove this try-except block when issues are resolved. + return + + # And check that we got equivalent and stable output. + black.assert_equivalent(src_contents, dst_contents) + black.assert_stable(src_contents, dst_contents, mode=mode) + + # Future test: check that pure-python and mypyc versions of black + # give identical output for identical input? + + +if __name__ == "__main__": + test_idempotent_any_syntatically_valid_python() From fd5928c589267b5df5fffbe408ec0b97a558f70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Aug 2020 14:11:18 +0200 Subject: [PATCH 033/680] Update the changelog --- CHANGES.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index eb6d1c2ebbf..d36d8463a45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,15 +7,44 @@ - re-implemented support for explicit trailing commas: now it works consistently within any bracket pair, including nested structures (#1288 and duplicates) -- reindent docstrings when reindenting code around it (#1053) +- `Black` now reindents docstrings when reindenting code around it (#1053) -- show colored diffs (#1266) +- `Black` now shows colored diffs (#1266) -- move to 'py3' tagged wheels (#1388) +- `Black` is now packaged using 'py3' tagged wheels (#1388) -- remove deprecated `--py36` option (#1236) +- `Black` now supports Python 3.8 code, e.g. star expressions in return statements + (#1121) -- add `--force-exclude` argument (#1032) +- `Black` no longer normalizes capital R-string prefixes as those have a + community-accepted meaning (#1244) + +- `Black` now uses exit code 2 when specified configuration file doesn't exit (#1361) + +- `Black` now works on AWS Lambda (#1141) + +- added `--force-exclude` argument (#1032) + +- removed deprecated `--py36` option (#1236) + +- fixed `--diff` output when EOF is encountered (#526) + +- fixed `# fmt: off` handling around decorators (#560) + +- fixed unstable formatting with some `# type: ignore` comments (#1113) + +- fixed invalid removal on organizing brackets followed by indexing (#1575) + +- introduced `black-primer`, a CI tool that allows us to run regression tests against + existing open source users of Black (#1402) + +- introduced property-based fuzzing to our test suite based on Hypothesis and + Hypothersmith (#1566) + +- implemented experimental and disabled by default long string rewrapping (#1132), + hidden under a `--experimental-string-processing` flag while it's being worked on; + this is an undocumented and unsupported feature, you lose Internet points for + depending on it (#1609) #### Vim plugin From 5faabb56160599c5303e53cf6113f1becc69756d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Aug 2020 14:33:16 +0200 Subject: [PATCH 034/680] Make doc generation a little smarter, update doc sections --- docs/authors.md | 1 + docs/change_log.md | 39 ++++++++++++++++++++++++++++++++++----- docs/conf.py | 19 +++++++++++++------ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/docs/authors.md b/docs/authors.md index 6a3a8d63f91..a5349b4b9df 100644 --- a/docs/authors.md +++ b/docs/authors.md @@ -181,3 +181,4 @@ Multiple contributions by: - Yazdan - [Yngve Høiseth](mailto:yngve@hoiseth.net) - [Yurii Karabas](mailto:1998uriyyo@gmail.com) +- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/docs/change_log.md b/docs/change_log.md index 3cc8c40d8c6..5b7f08e101d 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -9,15 +9,44 @@ - re-implemented support for explicit trailing commas: now it works consistently within any bracket pair, including nested structures (#1288 and duplicates) -- reindent docstrings when reindenting code around it (#1053) +- `Black` now reindents docstrings when reindenting code around it (#1053) -- show colored diffs (#1266) +- `Black` now shows colored diffs (#1266) -- move to 'py3' tagged wheels (#1388) +- `Black` is now packaged using 'py3' tagged wheels (#1388) -- remove deprecated `--py36` option (#1236) +- `Black` now supports Python 3.8 code, e.g. star expressions in return statements + (#1121) -- add `--force-exclude` argument (#1032) +- `Black` no longer normalizes capital R-string prefixes as those have a + community-accepted meaning (#1244) + +- `Black` now uses exit code 2 when specified configuration file doesn't exit (#1361) + +- `Black` now works on AWS Lambda (#1141) + +- added `--force-exclude` argument (#1032) + +- removed deprecated `--py36` option (#1236) + +- fixed `--diff` output when EOF is encountered (#526) + +- fixed `# fmt: off` handling around decorators (#560) + +- fixed unstable formatting with some `# type: ignore` comments (#1113) + +- fixed invalid removal on organizing brackets followed by indexing (#1575) + +- introduced `black-primer`, a CI tool that allows us to run regression tests against + existing open source users of Black (#1402) + +- introduced property-based fuzzing to our test suite based on Hypothesis and + Hypothersmith (#1566) + +- implemented experimental and disabled by default long string rewrapping (#1132), + hidden under a `--experimental-string-processing` flag while it's being worked on; + this is an undocumented and unsupported feature, you lose Internet points for + depending on it (#1609) #### Vim plugin diff --git a/docs/conf.py b/docs/conf.py index 8dd77b41d61..7381c9d6423 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ from pathlib import Path import re import string -from typing import Callable, List, Optional, Pattern, Tuple, Set +from typing import Callable, Dict, List, Optional, Pattern, Tuple, Set from dataclasses import dataclass import logging @@ -99,7 +99,13 @@ def get_contents(section: DocSection) -> str: for lineno, line in enumerate(f, start=1): if lineno >= start_line and lineno < end_line: contents.append(line) - return "".join(contents) + result = "".join(contents) + # Let's make Prettier happy with the amount of trailing newlines in the sections. + if result.endswith("\n\n"): + result = result[:-1] + if not result.endswith("\n"): + result = result + "\n" + return result def get_sections_from_readme() -> List[DocSection]: @@ -159,18 +165,19 @@ def process_sections( It processes custom sections before the README generated sections so sections in the README can be overwritten with custom options. """ - processed_sections: Set[str] = set() + processed_sections: Dict[str, DocSection] = {} modified_files: Set[Path] = set() sections: List[DocSection] = custom_sections sections.extend(readme_sections) for section in sections: - LOG.info(f"Processing '{section.name}' from {section.src}") if section.name in processed_sections: - LOG.info( + LOG.warning( f"Skipping '{section.name}' from '{section.src}' as it is a duplicate" + f" of a custom section from '{processed_sections[section.name].src}'" ) continue + LOG.info(f"Processing '{section.name}' from '{section.src}'") target_path: Path = CURRENT_DIR / section.get_out_filename() if target_path in modified_files: LOG.warning( @@ -188,7 +195,7 @@ def process_sections( rel = section.src.resolve().relative_to(CURRENT_DIR.parent) f.write(f'[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM {rel}"\n\n') f.write(contents) - processed_sections.add(section.name) + processed_sections[section.name] = section modified_files.add(target_path) From 292bceb9fd2d7a642351d06c02d2e751933baa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Aug 2020 18:48:11 +0200 Subject: [PATCH 035/680] Add more trailing comma test variants --- tests/data/comments7.py | 142 ++++++++ tests/data/composition_no_trailing_comma.py | 367 ++++++++++++++++++++ tests/test_black.py | 8 + 3 files changed, 517 insertions(+) create mode 100644 tests/data/composition_no_trailing_comma.py diff --git a/tests/data/comments7.py b/tests/data/comments7.py index a7bd281c91e..0e2bd35bf55 100644 --- a/tests/data/comments7.py +++ b/tests/data/comments7.py @@ -22,6 +22,12 @@ # resolve_to_config_type, # DEFAULT_TYPE_ATTRIBUTES, ) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component # DRY +) result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -46,6 +52,26 @@ def func(): 0.0789, a[-1], # type: ignore ) + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0789, + a[-1] # type: ignore + ) + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1] # type: ignore + ) # The type: ignore exception only applies to line length, not # other types of formatting. @@ -55,6 +81,54 @@ def func(): ) +class C: + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "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" + ), + # 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" + ), + ( + {"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" + ), + # 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" + ), + ( + {"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" + ) + ] + ) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + ... + # output from .config import ( @@ -81,6 +155,12 @@ def func(): # resolve_to_config_type, # DEFAULT_TYPE_ATTRIBUTES, ) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -105,6 +185,19 @@ def func(): 0.0789, a[-1], # type: ignore ) + c = call(0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1]) # type: ignore + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1], # type: ignore + ) # The type: ignore exception only applies to line length, not # other types of formatting. @@ -122,3 +215,52 @@ def func(): "aaaaaaaa", "aaaaaaaa", ) + + +class C: + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "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", + ), + # 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", + ), + ( + {"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", + ), + # 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", + ), + ( + {"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", + ), + ], + ) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + ... \ No newline at end of file diff --git a/tests/data/composition_no_trailing_comma.py b/tests/data/composition_no_trailing_comma.py new file mode 100644 index 00000000000..f17b89dea8d --- /dev/null +++ b/tests/data/composition_no_trailing_comma.py @@ -0,0 +1,367 @@ +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + # Another + ): + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } + + def tricky_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ), "Not what we expected" + + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected, ( + "Not what we expected and the message is too long to fit in one line" + ) + + assert expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ) == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, ( + "Not what we expected and the message is too long to fit in one line" + " because it's too long" + ) + + 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, + ) + + assert ( + expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect + == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } + ) + + + +# output + +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + # Another + ): + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } + + def tricky_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ), "Not what we expected" + + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected, ( + "Not what we expected and the message is too long to fit in one line" + ) + + assert expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ) == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, ( + "Not what we expected and the message is too long to fit in one line" + " because it's too long" + ) + + 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, + ) + + assert ( + expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect + == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } + ) diff --git a/tests/test_black.py b/tests/test_black.py index f89f1403aba..bb1929ebb31 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -630,6 +630,14 @@ def test_composition(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) + @patch("black.dump_to_file", dump_to_stderr) + def test_composition_no_trailing_comma(self) -> None: + source, expected = read_data("composition_no_trailing_comma") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) + @patch("black.dump_to_file", dump_to_stderr) def test_empty_lines(self) -> None: source, expected = read_data("empty_lines") From d46268cd671760d4c370476006795919951b1076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Aug 2020 21:33:51 +0200 Subject: [PATCH 036/680] Run trailing comma tests with TargetVersion.PY38 --- tests/test_black.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index bb1929ebb31..16002c0b728 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -593,7 +593,8 @@ def test_comments6(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_comments7(self) -> None: source, expected = read_data("comments7") - actual = fs(source) + mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) @@ -633,7 +634,8 @@ def test_composition(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_composition_no_trailing_comma(self) -> None: source, expected = read_data("composition_no_trailing_comma") - actual = fs(source) + mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) From 586d24236e6b57bc3b5da85fdbe2563835021076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Aug 2020 18:29:59 +0200 Subject: [PATCH 037/680] Address pre-existing trailing commas when not in the rightmost bracket pair This required some hackery. Long story short, we need to reuse the ability to omit rightmost bracket pairs (which glues them together and splits on something else instead), for use with pre-existing trailing commas. This form of user-controlled formatting is brittle so we have to be careful not to cause a scenario where Black first formats code without trailing commas in one way, and then looks at the same file with pre-existing trailing commas (that it itself put on the previous run) and decides to format the code again. One particular ugly edge case here is handling of optional parentheses. In particular, the long-standing `line_length=1` hack got in the way of pre-existing trailing commas and had to be removed. Instead, a more intelligent but costly solution was put in place: a "second opinion" if the formatting that omits optional parentheses ended up causing lines to be too long. Again, for efficiency purposes, Black reuses Leaf objects from blib2to3 and modifies them in place, which was invalid for having two separate formattings. Line cloning was used to mitigate this. Fixes #1619 --- src/black/__init__.py | 243 +++++++++++++++++----- tests/data/cantfit.py | 12 +- tests/data/function_trailing_comma.py | 21 ++ tests/data/function_trailing_comma_wip.py | 5 - tests/data/long_strings_flag_disabled.py | 13 +- tests/test_black.py | 16 +- 6 files changed, 235 insertions(+), 75 deletions(-) delete mode 100644 tests/data/function_trailing_comma_wip.py diff --git a/src/black/__init__.py b/src/black/__init__.py index faa88b38af7..e37caa98a2c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -195,6 +195,7 @@ class Feature(Enum): ASYNC_KEYWORDS = 7 ASSIGNMENT_EXPRESSIONS = 8 POS_ONLY_ARGUMENTS = 9 + FORCE_OPTIONAL_PARENTHESES = 50 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { @@ -1284,6 +1285,7 @@ class BracketTracker: previous: Optional[Leaf] = None _for_loop_depths: List[int] = field(default_factory=list) _lambda_argument_depths: List[int] = field(default_factory=list) + invisible: List[Leaf] = field(default_factory=list) def mark(self, leaf: Leaf) -> None: """Mark `leaf` with bracket-related metadata. Keep track of delimiters. @@ -1309,6 +1311,8 @@ def mark(self, leaf: Leaf) -> None: self.depth -= 1 opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) leaf.opening_bracket = opening_bracket + if not leaf.value: + self.invisible.append(leaf) leaf.bracket_depth = self.depth if self.depth == 0: delim = is_split_before_delimiter(leaf, self.previous) @@ -1321,6 +1325,8 @@ def mark(self, leaf: Leaf) -> None: if leaf.type in OPENING_BRACKETS: self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf self.depth += 1 + if not leaf.value: + self.invisible.append(leaf) self.previous = leaf self.maybe_increment_lambda_arguments(leaf) self.maybe_increment_for_loop_variable(leaf) @@ -2627,20 +2633,31 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: else: def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: + """Wraps calls to `right_hand_split`. + + The calls increasingly `omit` right-hand trailers (bracket pairs with + content), meaning the trailers get glued together to split on another + 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) ) + # 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): yield from lines return # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition - # reported as not fitting a single line. - # line_length=1 here was historically a bug that somehow became a feature. - # See #762 and #781 for the full story. - yield from right_hand_split(line, line_length=1, features=features) + # reported as not fitting a single line, as well as lines that contain + # pre-existing trailing commas (those have to be exploded). + yield from right_hand_split( + line, line_length=mode.line_length, features=features + ) if mode.experimental_string_processing: if line.inside_brackets: @@ -2671,17 +2688,8 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: # We are accumulating lines in `result` because we might want to abort # mission and return the original line in the end, or attempt a different # split altogether. - result: List[Line] = [] try: - for transformed_line in transform(line, features): - if str(transformed_line).strip("\n") == line_str: - raise CannotTransform( - "Line transformer returned an unchanged result" - ) - - result.extend( - transform_line(transformed_line, mode=mode, features=features) - ) + result = run_transformer(line, transform, mode, features, line_str=line_str) except CannotTransform: continue else: @@ -2722,6 +2730,7 @@ class StringTransformer(ABC): line_length: int normalize_strings: bool + __name__ = "StringTransformer" @abstractmethod def do_match(self, line: Line) -> TMatchResult: @@ -2968,7 +2977,7 @@ def __remove_backslash_line_continuation_chars( ) new_line = line.clone() - new_line.comments = line.comments + new_line.comments = line.comments.copy() append_leaves(new_line, line, LL) new_string_leaf = new_line.leaves[string_idx] @@ -3296,7 +3305,6 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: 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) @@ -4740,8 +4748,9 @@ def right_hand_split( tail = bracket_split_build_line(tail_leaves, line, opening_bracket) bracket_split_succeeded_or_raise(head, body, tail) if ( + Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren - opening_bracket.type == token.LPAR + and opening_bracket.type == token.LPAR and not opening_bracket.value # the closing bracket is an optional paren and closing_bracket.type == token.RPAR @@ -4752,7 +4761,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) + and can_omit_invisible_parens(body, line_length, omit_on_explode=omit) ): omit = {id(closing_bracket), *omit} try: @@ -5587,6 +5596,9 @@ def should_split_body_explode(line: Line, opening_bracket: Leaf) -> 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: + return False + depth = closing.bracket_depth + 1 for _opening_index, leaf in enumerate(leaves): if leaf is opening: @@ -5678,11 +5690,13 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf a preceding closing bracket fits in one line. Yielded sets are cumulative (contain results of previous yields, too). First - set is empty. + set is empty, unless the line should explode, in which case bracket pairs until + the one that needs to explode are omitted. """ omit: Set[LeafID] = set() - yield omit + if not line.should_explode: + yield omit length = 4 * line.depth opening_bracket: Optional[Leaf] = None @@ -5701,9 +5715,24 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf if leaf is opening_bracket: opening_bracket = None elif leaf.type in CLOSING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if ( + line.should_explode + and prev + and prev.type == token.COMMA + and not prev.was_checked + and not is_one_tuple_between( + leaf.opening_bracket, leaf, line.leaves + ) + ): + # Never omit bracket pairs with pre-existing trailing commas. + # We need to explode on those. + break + inner_brackets.add(id(leaf)) elif leaf.type in CLOSING_BRACKETS: - if index > 0 and line.leaves[index - 1].type in OPENING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if prev and prev.type in OPENING_BRACKETS: # Empty brackets would fail a split so treat them as "inner" # brackets (e.g. only add them to the `omit` set if another # pair of brackets was good enough. @@ -5716,6 +5745,17 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf inner_brackets.clear() yield omit + if ( + line.should_explode + and prev + and prev.type == token.COMMA + and not prev.was_checked + and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) + ): + # Never omit bracket pairs with pre-existing trailing commas. + # We need to explode on those. + break + if leaf.value: opening_bracket = leaf.opening_bracket closing_bracket = leaf @@ -6297,7 +6337,11 @@ def can_be_split(line: Line) -> bool: return True -def can_omit_invisible_parens(line: Line, line_length: int) -> 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? Returns True for only a subset of potentially nice looking formattings but @@ -6320,37 +6364,27 @@ def can_omit_invisible_parens(line: Line, line_length: int) -> bool: assert len(line.leaves) >= 2, "Stranded delimiter" - first = line.leaves[0] - second = line.leaves[1] - penultimate = line.leaves[-2] - last = line.leaves[-1] - # With a single delimiter, omit if the expression starts or ends with # a bracket. + first = line.leaves[0] + second = line.leaves[1] if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS: - remainder = False - length = 4 * line.depth - for _index, leaf, leaf_length in enumerate_with_length(line): - if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: - remainder = True - if remainder: - length += leaf_length - if length > line_length: - break - - if leaf.type in OPENING_BRACKETS: - # There are brackets we can further split on. - remainder = False - - else: - # checked the entire string and line length wasn't exceeded - if len(line.leaves) == _index + 1: - return True + if _can_omit_opening_paren(line, first=first, line_length=line_length): + return True # Note: we are not returning False here because a line might have *both* # a leading opening bracket and a trailing closing bracket. If the # opening bracket doesn't match our rule, maybe the closing will. + penultimate = line.leaves[-2] + last = line.leaves[-1] + if line.should_explode: + 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 or last.type == token.RBRACE @@ -6371,21 +6405,124 @@ def can_omit_invisible_parens(line: Line, line_length: int) -> bool: # unnecessary. return True - length = 4 * line.depth - seen_other_brackets = False - for _index, leaf, leaf_length in enumerate_with_length(line): + if ( + line.should_explode + and penultimate.type == token.COMMA + and not penultimate.was_checked + ): + # 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 + + return False + + +def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + remainder = False + length = 4 * line.depth + _index = -1 + for _index, leaf, leaf_length in enumerate_with_length(line): + if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: + remainder = True + if remainder: length += leaf_length - if leaf is last.opening_bracket: - if seen_other_brackets or length <= line_length: - return True + if length > line_length: + break - elif leaf.type in OPENING_BRACKETS: + if leaf.type in OPENING_BRACKETS: # There are brackets we can further split on. - seen_other_brackets = True + remainder = False + + else: + # checked the entire string and line length wasn't exceeded + if len(line.leaves) == _index + 1: + return True + + return False + + +def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + length = 4 * line.depth + seen_other_brackets = False + for _index, leaf, leaf_length in enumerate_with_length(line): + length += leaf_length + if leaf is last.opening_bracket: + if seen_other_brackets or length <= line_length: + return True + + elif leaf.type in OPENING_BRACKETS: + # There are brackets we can further split on. + seen_other_brackets = True return False +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 + 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 run_transformer( + line: Line, + transform: Transformer, + mode: Mode, + features: Collection[Feature], + *, + line_str: str = "", +) -> List[Line]: + if not line_str: + line_str = line_to_string(line) + result: List[Line] = [] + for transformed_line in transform(line, features): + if str(transformed_line).strip("\n") == line_str: + raise CannotTransform("Line transformer returned an unchanged result") + + result.extend(transform_line(transformed_line, mode=mode, features=features)) + + if not ( + transform.__name__ == "rhs" + and line.bracket_tracker.invisible + and not any(bracket.value for bracket in line.bracket_tracker.invisible) + and not line.contains_multiline_strings() + and not result[0].contains_uncollapsable_type_comments() + and not result[0].contains_unsplittable_type_ignore() + and not is_line_short_enough(result[0], line_length=mode.line_length) + ): + return result + + line_copy = line.clone() + append_leaves(line_copy, line, line.leaves) + features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} + 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 + ): + result = second_opinion + return result + + def get_cache_file(mode: Mode) -> Path: return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle" diff --git a/tests/data/cantfit.py b/tests/data/cantfit.py index ef9b78e09a9..0849374f776 100644 --- a/tests/data/cantfit.py +++ b/tests/data/cantfit.py @@ -67,11 +67,15 @@ normal_name = ( but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying() ) -normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( - arg1, arg2, arg3 +normal_name = ( + but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( + arg1, arg2, arg3 + ) ) -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 +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( diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index 314a56cf67b..d15459cbeb5 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -9,6 +9,12 @@ def f2(a,b,): def f(a:int=1,): call(arg={'explode': 'this',}) call2(arg=[1,2,3],) + x = { + "a": 1, + "b": 2, + }["a"] + if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: + pass def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -51,6 +57,21 @@ def f( call2( arg=[1, 2, 3], ) + x = { + "a": 1, + "b": 2, + }["a"] + if a == { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + }["a"]: + pass def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ diff --git a/tests/data/function_trailing_comma_wip.py b/tests/data/function_trailing_comma_wip.py deleted file mode 100644 index c41fc709d97..00000000000 --- a/tests/data/function_trailing_comma_wip.py +++ /dev/null @@ -1,5 +0,0 @@ -CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final - -# output - -CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final \ No newline at end of file diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/long_strings_flag_disabled.py index db3954e3abd..ef3094fd779 100644 --- a/tests/data/long_strings_flag_disabled.py +++ b/tests/data/long_strings_flag_disabled.py @@ -133,14 +133,11 @@ "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/test_black.py b/tests/test_black.py index 16002c0b728..6705490ea13 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -346,11 +346,16 @@ def test_function2(self) -> None: black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) - def test_function_trailing_comma_wip(self) -> None: - source, expected = read_data("function_trailing_comma_wip") - # sys.settrace(tracefunc) - actual = fs(source) - # sys.settrace(None) + def _test_wip(self) -> None: + source, expected = read_data("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()) @@ -2085,6 +2090,7 @@ def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable: return tracefunc stack = len(inspect.stack()) - 19 + stack *= 2 filename = frame.f_code.co_filename lineno = frame.f_lineno func_sig_lineno = lineno - 1 From 9270a10f6f59f069eb14ffba0c75f58e5895b27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 25 Aug 2020 22:26:13 +0200 Subject: [PATCH 038/680] Improve docstring re-indentation handling This addresses a few crashers, namely: * producing non-equivalent code due to mangling escaped newlines, * invalid hugging quote characters in the docstring body to the docstring outer triple quotes (causing a quadruple quote which is a syntax error), * lack of handling for docstrings that start on the same line as the `def`, and * invalid stripping of outer triple quotes when the docstring contained a string prefix. As a bonus, tests now also run when string normalization is disabled. --- src/black/__init__.py | 42 +++++++++++++++++++++++------ tests/data/docstring.py | 59 +++++++++++++++++++++++++++++++++++++++++ tests/test_black.py | 5 ++++ 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index e37caa98a2c..c3c8c207cd4 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2037,13 +2037,20 @@ def visit_factor(self, node: Node) -> Iterator[Line]: yield from self.visit_default(node) def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: - # Check if it's a docstring - if prev_siblings_are( - leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] - ) and is_multiline_string(leaf): - prefix = " " * self.current_line.depth - docstring = fix_docstring(leaf.value[3:-3], prefix) - leaf.value = leaf.value[0:3] + docstring + leaf.value[-3:] + 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. + prefix = get_string_prefix(leaf.value) + lead_len = len(prefix) + 3 + tail_len = -3 + indent = " " * 4 * self.current_line.depth + docstring = fix_docstring(leaf.value[lead_len:tail_len], indent) + if docstring: + if leaf.value[lead_len - 1] == docstring[0]: + docstring = " " + docstring + if leaf.value[tail_len + 1] == docstring[-1]: + docstring = docstring + " " + leaf.value = leaf.value[0:lead_len] + docstring + leaf.value[tail_len:] normalize_string_quotes(leaf) yield from self.visit_default(leaf) @@ -6608,6 +6615,26 @@ def patched_main() -> None: main() +def is_docstring(leaf: Leaf) -> bool: + if not is_multiline_string(leaf): + # For the purposes of docstring re-indentation, we don't need to do anything + # with single-line docstrings. + return False + + if prev_siblings_are( + leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] + ): + return True + + # Multiline docstring on the same line as the `def`. + if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]): + # `syms.parameters` is only used in funcdefs and async_funcdefs in the Python + # grammar. We're safe to return True without further checks. + return True + + return False + + def fix_docstring(docstring: str, prefix: str) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: @@ -6631,7 +6658,6 @@ def fix_docstring(docstring: str, prefix: str) -> str: trimmed.append(prefix + stripped_line) else: trimmed.append("") - # Return a single string: return "\n".join(trimmed) diff --git a/tests/data/docstring.py b/tests/data/docstring.py index fcb8eb12a78..2d3d73a101c 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -81,6 +81,35 @@ def single_line(): """ pass + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + # output class MyClass: @@ -164,3 +193,33 @@ def over_indent(): def single_line(): """But with a newline after it!""" pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 6705490ea13..cf311f52e14 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -496,6 +496,11 @@ def test_docstring(self) -> None: self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) + mode = replace(DEFAULT_MODE, string_normalization=False) + not_normalized = fs(source, mode=mode) + self.assertFormatEqual(expected, not_normalized) + black.assert_equivalent(source, not_normalized) + black.assert_stable(source, not_normalized, mode=mode) def test_long_strings(self) -> None: """Tests for splitting long strings.""" From 4a065a43e1f641a8c5e9266d77feba810e8905ac Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 25 Aug 2020 18:54:05 -0700 Subject: [PATCH 039/680] Add links regarding Spotless integration for gradle/maven users (#1622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- docs/editor_integration.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index eb83a1a4b43..73107d6a4a1 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -255,6 +255,10 @@ Sublime Text, Visual Studio Code and many more), you can use the Use [python-black](https://atom.io/packages/python-black). +## Gradle (the build tool) + +Use the [Spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle) plugin. + ## Kakoune Add the following hook to your kakrc, then run _Black_ with `:format`. @@ -269,9 +273,9 @@ hook global WinSetOption filetype=python %{ Use [Thonny-black-code-format](https://github.com/Franccisco/thonny-black-code-format). -## Other editors +## Other integrations -Other editors will require external contributions. +Other editors and tools will require external contributions. Patches welcome! ✨ 🍰 ✨ From 89b776678a9953e08d2d6ae35f577789be9dde00 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 25 Aug 2020 21:55:05 -0700 Subject: [PATCH 040/680] Primer update config - enable pytest (#1626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reformatted projects I have acceess to: - aioexabgp - bandersnatch - flake8-bugbear ``` -- primer results 📊 -- 13 / 16 succeeded (81.25%) ✅ 0 / 16 FAILED (0.0%) 💩 - 3 projects disabled by config - 0 projects skipped due to Python version - 0 skipped due to long checkout ``` * Also re-enable pytest ``` -- primer results 📊 -- 14 / 16 succeeded (87.5%) ✅ 0 / 16 FAILED (0.0%) 💩 - 2 projects disabled by config - 0 projects skipped due to Python version - 0 skipped due to long checkout real 2m26.207s user 17m55.404s sys 0m43.061s ``` --- src/black_primer/primer.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 83a9cb50161..546f47782cd 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -3,7 +3,7 @@ "projects": { "aioexabgp": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, "py_versions": ["all"] @@ -17,7 +17,7 @@ }, "bandersnatch": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pypa/bandersnatch.git", "long_checkout": false, "py_versions": ["all"] @@ -30,7 +30,7 @@ "py_versions": ["all"] }, "django": { - "disabled_reason": "black --check --diff returned 123", + "disabled_reason": "black --check --diff returned 123 on two files", "disabled": true, "cli_arguments": [], "expect_formatting_changes": true, @@ -40,7 +40,7 @@ }, "flake8-bugbear": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", "long_checkout": false, "py_versions": ["all"] @@ -53,10 +53,10 @@ "py_versions": ["all"] }, "pandas": { - "disabled_reason": "black --check --diff returned 123", + "disabled_reason": "black --check --diff returned 123 on one file", "disabled": true, "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pandas-dev/pandas.git", "long_checkout": false, "py_versions": ["all"] @@ -83,10 +83,8 @@ "py_versions": ["all"] }, "pytest": { - "disabled_reason": "black --check --diff returned 123", - "disabled": true, "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pytest-dev/pytest.git", "long_checkout": false, "py_versions": ["all"] From 824d06f7204d36fc1afcf09a090c4e418e3d4cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 16:00:55 +0200 Subject: [PATCH 041/680] v20.8b0 --- CHANGES.md | 2 +- docs/change_log.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d36d8463a45..b475e90616e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ ## Change Log -### Unreleased +### 20.8b0 #### _Black_ diff --git a/docs/change_log.md b/docs/change_log.md index 5b7f08e101d..1219893899a 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -2,7 +2,7 @@ ## Change Log -### Unreleased +### 20.8b0 #### _Black_ From d7aa7f3cdd1e832204cd63a574a8935157e18de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 12:22:56 +0200 Subject: [PATCH 042/680] Treat all trailing commas as pre-existing, as they effectively are On a second pass of Black on the same file, inserted trailing commas are now pre-existing. Doesn't make sense to differentiate between the passes then. --- src/black/__init__.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index c3c8c207cd4..4d4f4b73b91 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1627,14 +1627,13 @@ def contains_multiline_strings(self) -> bool: def maybe_should_explode(self, closing: Leaf) -> bool: """Return True if this line should explode (always be split), that is when: - - there's a pre-existing trailing comma here; and + - there's a trailing comma here; and - it's not a one-tuple. """ if not ( closing.type in CLOSING_BRACKETS and self.leaves and self.leaves[-1].type == token.COMMA - and not self.leaves[-1].was_checked # pre-existing ): return False @@ -2661,7 +2660,7 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition # reported as not fitting a single line, as well as lines that contain - # pre-existing trailing commas (those have to be exploded). + # trailing commas (those have to be exploded). yield from right_hand_split( line, line_length=mode.line_length, features=features ) @@ -4855,7 +4854,6 @@ def bracket_split_build_line( if leaves[i].type != token.COMMA: new_comma = Leaf(token.COMMA, ",") - new_comma.was_checked = True leaves.insert(i + 1, new_comma) break @@ -4951,7 +4949,6 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: and current_line.leaves[-1].type != STANDALONE_COMMENT ): new_comma = Leaf(token.COMMA, ",") - new_comma.was_checked = True current_line.append(new_comma) yield current_line @@ -5584,20 +5581,20 @@ def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: # than one of them (we're excluding the trailing comma and if the delimiter priority # is still commas, that means there's more). exclude = set() - pre_existing_trailing_comma = False + trailing_comma = False try: last_leaf = line.leaves[-1] if last_leaf.type == token.COMMA: - pre_existing_trailing_comma = not last_leaf.was_checked + trailing_comma = True exclude.add(id(last_leaf)) max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) except (IndexError, ValueError): return False return max_priority == COMMA_PRIORITY and ( + trailing_comma # always explode imports - opening_bracket.parent.type in {syms.atom, syms.import_from} - or pre_existing_trailing_comma + or opening_bracket.parent.type in {syms.atom, syms.import_from} ) @@ -5727,12 +5724,11 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf line.should_explode and prev and prev.type == token.COMMA - and not prev.was_checked and not is_one_tuple_between( leaf.opening_bracket, leaf, line.leaves ) ): - # Never omit bracket pairs with pre-existing trailing commas. + # Never omit bracket pairs with trailing commas. # We need to explode on those. break @@ -5756,10 +5752,9 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf line.should_explode and prev and prev.type == token.COMMA - and not prev.was_checked and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) ): - # Never omit bracket pairs with pre-existing trailing commas. + # Never omit bracket pairs with trailing commas. # We need to explode on those. break @@ -6412,11 +6407,7 @@ def can_omit_invisible_parens( # unnecessary. return True - if ( - line.should_explode - and penultimate.type == token.COMMA - and not penultimate.was_checked - ): + if line.should_explode and penultimate.type == token.COMMA: # The rightmost non-omitted bracket pair is the one we want to explode on. return True From 30a332c32fabe13d883d86b0422bcded0c91642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 12:57:05 +0200 Subject: [PATCH 043/680] Include mode information for unstable formattings --- src/black/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/__init__.py b/src/black/__init__.py index 4d4f4b73b91..200e31fd458 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6163,6 +6163,7 @@ def assert_stable(src: str, dst: str, mode: Mode) -> None: newdst = format_str(dst, mode=mode) if dst != newdst: log = dump_to_file( + str(mode), diff(src, dst, "source", "first pass"), diff(dst, newdst, "first pass", "second pass"), ) From ceeb1d9a2ee08190704076f616e74a3cdd5e10c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 12:57:38 +0200 Subject: [PATCH 044/680] Add expected failure tests with the unstable formattings --- tests/data/trailing_comma_optional_parens1.py | 3 +++ tests/data/trailing_comma_optional_parens2.py | 3 +++ tests/data/trailing_comma_optional_parens3.py | 8 +++++++ tests/test_black.py | 21 +++++++++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 tests/data/trailing_comma_optional_parens1.py create mode 100644 tests/data/trailing_comma_optional_parens2.py create mode 100644 tests/data/trailing_comma_optional_parens3.py diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/trailing_comma_optional_parens1.py new file mode 100644 index 00000000000..5ad29a8affd --- /dev/null +++ b/tests/data/trailing_comma_optional_parens1.py @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000000..2817073816e --- /dev/null +++ b/tests/data/trailing_comma_optional_parens2.py @@ -0,0 +1,3 @@ +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 diff --git a/tests/data/trailing_comma_optional_parens3.py b/tests/data/trailing_comma_optional_parens3.py new file mode 100644 index 00000000000..e6a673ec537 --- /dev/null +++ b/tests/data/trailing_comma_optional_parens3.py @@ -0,0 +1,8 @@ +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 cf311f52e14..f5d4e1115a8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -368,6 +368,27 @@ def test_function_trailing_comma(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) + @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_expression(self) -> None: source, expected = read_data("expression") From 3ae83c3090954c45b805010bc04354c2fbe3f2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 17:15:20 +0200 Subject: [PATCH 045/680] Make dependency on Click 7.0, regex 2020.1.8, and toml 0.10.1 explicit --- Pipfile | 4 ++-- Pipfile.lock | 53 ++++++++++++++++++++++++++-------------------------- setup.py | 5 ++--- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Pipfile b/Pipfile index 18e8a545617..44f57f6773c 100644 --- a/Pipfile +++ b/Pipfile @@ -24,10 +24,10 @@ black = {editable = true, extras = ["d"], path = "."} aiohttp = ">=3.3.2" aiohttp-cors = "*" appdirs = "*" -click = ">=6.5" +click = ">=7.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.6" -regex = ">=2019.8" +regex = ">=2020.1.8" toml = ">=0.10.1" typed-ast = "==1.4.0" typing_extensions = ">=3.7.4" diff --git a/Pipfile.lock b/Pipfile.lock index ddbf9b99520..32b8012ff0e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "682054eb4a3d4366e2f76b3ae74286d156a270c0d7b57299a81f8cc1d0a51d19" + "sha256": "61d09a6b8a8c310becd5e108ed08e0eeae50c7323c08c8040367abded0cb1031" }, "pipfile-spec": 6, "requires": {}, @@ -186,10 +186,10 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" @@ -222,12 +222,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", - "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", - "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.2" + "version": "==3.7.4.3" }, "yarl": { "hashes": [ @@ -468,11 +468,11 @@ }, "identify": { "hashes": [ - "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", - "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7" + "sha256:9f5fcf22b665eaece583bd395b103c2769772a0f646ffabb5b1f155901b07de2", + "sha256:b1aa2e05863dc80242610d46a7b49105e2eafe00ef0c8ff311c1828680760c76" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.28" + "version": "==1.4.29" }, "idna": { "hashes": [ @@ -500,11 +500,11 @@ }, "keyring": { "hashes": [ - "sha256:22df6abfed49912fc560806030051067fba9f0069cffa79da72899aeea4ccbd5", - "sha256:e7a17caf40c40b6bb8c4772224a487e4a63013560ed0c521065aeba7ecd42182" + "sha256:182f94fc0381546489e3e4d90384a8c1d43cc09ffe2eb4a826e7312df6e1be7c", + "sha256:cd4d486803d55bdb13e2d453eb61dbbc984773e4f2b98a455aa85b1f4bc421e4" ], "markers": "python_version >= '3.6'", - "version": "==21.3.0" + "version": "==21.3.1" }, "markupsafe": { "hashes": [ @@ -605,9 +605,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "packaging": { "hashes": [ @@ -634,11 +635,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", - "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.1" }, "pycodestyle": { "hashes": [ @@ -847,10 +848,10 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" @@ -899,12 +900,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", - "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", - "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.2" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ diff --git a/setup.py b/setup.py index bff439c6d8c..12fde2568cf 100644 --- a/setup.py +++ b/setup.py @@ -68,10 +68,9 @@ def get_long_description() -> str: python_requires=">=3.6", zip_safe=False, install_requires=[ - "click>=6.5", - "attrs>=18.1.0", + "click>=7.1.2", "appdirs", - "toml>=0.9.4", + "toml>=0.10.1", "typed-ast>=1.4.0", "regex>=2020.1.8", "pathspec>=0.6, <1", From 235412635e91950c8ef2d9ebe777f97fffd4f01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 17:50:44 +0200 Subject: [PATCH 046/680] v20.8b1 --- CHANGES.md | 7 +++++++ docs/change_log.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b475e90616e..11341779f58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ ## Change Log +### 20.8b1 + +#### _Packaging_ + +- explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions + older than 7.0 + ### 20.8b0 #### _Black_ diff --git a/docs/change_log.md b/docs/change_log.md index 1219893899a..658414bf967 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -2,6 +2,13 @@ ## Change Log +### 20.8b1 + +#### _Packaging_ + +- explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions + older than 7.0 + ### 20.8b0 #### _Black_ From 20f74c20f7efe80f0b0199153934dddd80e21d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 26 Aug 2020 18:18:14 +0200 Subject: [PATCH 047/680] Stop running Primer on macOS as it's flaky on GitHub Actions --- .github/workflows/primer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index b5dea5e7139..9b10db0d285 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8] - os: [ubuntu-latest, macOS-latest, windows-latest] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 From 1ebe9b70c5624da964364841e30a2e3ffe109c4e Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Wed, 24 Jun 2020 00:33:05 +0300 Subject: [PATCH 048/680] Simplify black code by using generator expressions --- src/black/__init__.py | 44 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 200e31fd458..954b93c77d7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -641,10 +641,9 @@ def path_empty( """ Exit if there is no `src` provided for formatting """ - if len(src) == 0: - if verbose or not quiet: - out(msg) - ctx.exit(0) + if not src and (verbose or not quiet): + out(msg) + ctx.exit(0) def reformat_one( @@ -928,7 +927,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 src_contents.strip() == "": + if not src_contents.strip(): raise NothingChanged dst_contents = format_str(src_contents, mode=mode) @@ -1062,7 +1061,7 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: """Given a string with source, return the lib2to3 Node.""" - if src_txt[-1:] != "\n": + if not src_txt.endswith("\n"): src_txt += "\n" for grammar in get_grammars(set(target_versions)): @@ -1547,11 +1546,10 @@ def is_triple_quoted_string(self) -> bool: def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> 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: - return True - - return False + return any( + leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit + for leaf in self.leaves + ) def contains_uncollapsable_type_comments(self) -> bool: ignored_ids = set() @@ -3992,12 +3990,13 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): def do_splitter_match(self, line: Line) -> TMatchResult: LL = line.leaves - string_idx = None - string_idx = string_idx or self._return_match(LL) - string_idx = string_idx or self._else_match(LL) - string_idx = string_idx or self._assert_match(LL) - string_idx = string_idx or self._assign_match(LL) - string_idx = string_idx or self._dict_match(LL) + string_idx = ( + self._return_match(LL) + or self._else_match(LL) + or self._assert_match(LL) + or self._assign_match(LL) + or self._dict_match(LL) + ) if string_idx is not None: string_value = line.leaves[string_idx].value @@ -4196,7 +4195,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: is_valid_index = is_valid_index_factory(LL) insert_str_child = insert_str_child_factory(LL[string_idx]) - comma_idx = len(LL) - 1 + comma_idx = -1 ends_with_comma = False if LL[comma_idx].type == token.COMMA: ends_with_comma = True @@ -4444,11 +4443,10 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool: of the more common static analysis tools for python (e.g. mypy, flake8, pylint). """ - for comment in comment_list: - if comment.value.startswith(("# type:", "# noqa", "# pylint:")): - return True - - return False + return any( + comment.value.startswith(("# type:", "# noqa", "# pylint:")) + for comment in comment_list + ) def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]: From 4ca92ac91c944b252caf8c2265e5854f839a66f4 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Wed, 26 Aug 2020 09:35:21 +0300 Subject: [PATCH 049/680] Revert contains_standalone_comments function changes --- src/black/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 954b93c77d7..96dd0e46e7b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1546,10 +1546,11 @@ def is_triple_quoted_string(self) -> bool: def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool: """If so, needs to be split before emitting.""" - return any( - leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit - for leaf in self.leaves - ) + for leaf in self.leaves: + if leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit: + return True + + return False def contains_uncollapsable_type_comments(self) -> bool: ignored_ids = set() From e268cbaf668cc86b1e6aa52e52483cf4592e176c Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Wed, 26 Aug 2020 13:43:00 +0300 Subject: [PATCH 050/680] Revert contains_pragma_comment function changes --- src/black/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 96dd0e46e7b..f51a50d3612 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -4444,10 +4444,11 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool: of the more common static analysis tools for python (e.g. mypy, flake8, pylint). """ - return any( - comment.value.startswith(("# type:", "# noqa", "# pylint:")) - for comment in comment_list - ) + for comment in comment_list: + if comment.value.startswith(("# type:", "# noqa", "# pylint:")): + return True + + return False def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]: From 5f78401b2863c4c2e8417905fa8b8bf4d648510d Mon Sep 17 00:00:00 2001 From: Rowan Rodrik van der Molen Date: Sat, 11 Jul 2020 21:20:31 +0200 Subject: [PATCH 051/680] Fix g:black_fast and g:black_(skip)_string_normalization opts --- plugin/black.vim | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugin/black.vim b/plugin/black.vim index 4af044e7a68..3dd3f2151c3 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -41,8 +41,12 @@ endif if !exists("g:black_linelength") let g:black_linelength = 88 endif -if !exists("g:black_skip_string_normalization") - let g:black_skip_string_normalization = 0 +if !exists("g:black_string_normalization") + if exists("g:black_skip_string_normalization") + let g:black_string_normalization = !g:black_skip_string_normalization + else + let g:black_string_normalization = 1 + endif endif python3 << EndPython3 @@ -50,6 +54,7 @@ import collections import os import sys import vim +from distutils.util import strtobool class Flag(collections.namedtuple("FlagBase", "name, cast")): @@ -62,15 +67,13 @@ class Flag(collections.namedtuple("FlagBase", "name, cast")): name = self.var_name if name == "line_length": name = name.replace("_", "") - if name == "string_normalization": - name = "skip_" + name return "g:black_" + name FLAGS = [ Flag(name="line_length", cast=int), - Flag(name="fast", cast=bool), - Flag(name="string_normalization", cast=bool), + Flag(name="fast", cast=strtobool), + Flag(name="string_normalization", cast=strtobool), ] From 7fe19fac5bccc08dbcf21c293ab50f1fcda7716f Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 26 Aug 2020 18:17:59 -0400 Subject: [PATCH 052/680] Fix multiline docstring quote normalization The quotes of multiline docstrings are now only normalized when string normalization is off, instead of the string normalization setting being ignored and the quotes being *always* normalized. I had to make a new test case and data file since the current pair for docstrings only worked when there is no formatting difference between the formatting results with string normalization on and off. I needed to add tests for when there *are* differences between the two. So I split test_docstring's test code when string normalization is disabled into a new test case along with a new data file. --- CHANGES.md | 7 + docs/change_log.md | 7 + src/black/__init__.py | 1 - .../data/docstring_no_string_normalization.py | 209 ++++++++++++++++++ tests/test_black.py | 13 +- 5 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 tests/data/docstring_no_string_normalization.py diff --git a/CHANGES.md b/CHANGES.md index 11341779f58..7352b857075 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ ## Change Log +### Unreleased + +#### _Black_ + +- `Black` now respects `--skip-string-normalization` when normalizing multiline + docstring quotes (#1637) + ### 20.8b1 #### _Packaging_ diff --git a/docs/change_log.md b/docs/change_log.md index 658414bf967..b7337166659 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -2,6 +2,13 @@ ## Change Log +### Unreleased + +#### _Black_ + +- `Black` now respects `--skip-string-normalization` when normalizing multiline + docstring quotes (#1637) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index f51a50d3612..34d8145c0f7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2049,7 +2049,6 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if leaf.value[tail_len + 1] == docstring[-1]: docstring = docstring + " " leaf.value = leaf.value[0:lead_len] + docstring + leaf.value[tail_len:] - normalize_string_quotes(leaf) yield from self.visit_default(leaf) diff --git a/tests/data/docstring_no_string_normalization.py b/tests/data/docstring_no_string_normalization.py new file mode 100644 index 00000000000..0457fcf114f --- /dev/null +++ b/tests/data/docstring_no_string_normalization.py @@ -0,0 +1,209 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + +# output + +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass diff --git a/tests/test_black.py b/tests/test_black.py index f5d4e1115a8..629afc5b0ad 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -517,11 +517,16 @@ def test_docstring(self) -> None: self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_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) - not_normalized = fs(source, mode=mode) - self.assertFormatEqual(expected, not_normalized) - black.assert_equivalent(source, not_normalized) - black.assert_stable(source, not_normalized, mode=mode) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode) def test_long_strings(self) -> None: """Tests for splitting long strings.""" From 2b75f8870eac943c93e93c740c9d2ef74efeeb41 Mon Sep 17 00:00:00 2001 From: mbarkhau Date: Thu, 27 Aug 2020 11:47:59 +0000 Subject: [PATCH 053/680] fix 1631 and add test (#1641) --- CHANGES.md | 2 ++ src/black/__init__.py | 3 ++- tests/test_black.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7352b857075..1c53604d4d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- fixed a crash when PWD=/ on POSIX (#1631) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 34d8145c0f7..048e771ce96 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5831,7 +5831,8 @@ def normalize_path_maybe_ignore( `report` is where "path ignored" output goes. """ try: - normalized_path = path.resolve().relative_to(root).as_posix() + 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}") return None diff --git a/tests/test_black.py b/tests/test_black.py index 629afc5b0ad..bc80c8fca8b 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -9,6 +9,7 @@ from io import BytesIO, TextIOWrapper import os from pathlib import Path +from platform import system import regex as re import sys from tempfile import TemporaryDirectory @@ -1939,6 +1940,23 @@ def test_find_project_root(self) -> None: self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) + def test_bpo_33660_workaround(self) -> None: + if system() == "Windows": + return + + # https://bugs.python.org/issue33660 + + old_cwd = Path.cwd() + try: + root = Path("/") + os.chdir(str(root)) + path = Path("workspace") / "project" + report = black.Report(verbose=True) + normalized_path = black.normalize_path_maybe_ignore(path, root, report) + self.assertEqual(normalized_path, "workspace/project") + finally: + os.chdir(str(old_cwd)) + class BlackDTestCase(AioHTTPTestCase): async def get_application(self) -> web.Application: From 0c6d4ca0c33aa97202e0e94a4a2389cc432597bc Mon Sep 17 00:00:00 2001 From: Tristan Seligmann Date: Fri, 28 Aug 2020 17:37:37 +0200 Subject: [PATCH 054/680] Fix typo in comment (#1650) Co-authored-by: Hugo van Kemenade --- fuzz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzz.py b/fuzz.py index fdd4917f2ec..604e6ced291 100644 --- a/fuzz.py +++ b/fuzz.py @@ -42,8 +42,8 @@ def test_idempotent_any_syntatically_valid_python( try: 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 code with it. See issues #970, #1012, #1358, and #1557. + # 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. # TODO: remove this try-except block when issues are resolved. return From 573b8de54470dbad8bcaaebd5a28dad507c44666 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 31 Aug 2020 14:18:43 -0700 Subject: [PATCH 055/680] Remove flake8 W503 from docs as it is ignored by default (#1661) Fixes #1660 --- .flake8 | 4 ++-- docs/compatible_configs.md | 20 +++++++------------- docs/the_black_code_style.md | 10 +++------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.flake8 b/.flake8 index cee6db4446b..656c0df24ee 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] -ignore = E203, E266, E501, W503 +extend-ignore = E203, E266, E501 # line length is intentionally set to 80 here because black uses Bugbear -# See https://github.com/psf/black/blob/master/README.md#line-length for more details +# See https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-length for more details max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index 25e959e3281..82f13932b0b 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -32,7 +32,7 @@ line_length = 88 _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 -[found here](https://github.com/psf/black#how-black-wraps-lines). +[found here](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#how-black-wraps-lines). isort's default mode of wrapping imports that extend past the `line_length` limit is "Grid". @@ -146,21 +146,15 @@ There are a few deviations that cause incompatibilities with _Black_. ``` max-line-length = 88 -extend-ignore = E203, W503 +extend-ignore = E203 ``` ### Why those options above? -When breaking a line, _Black_ will break it before a binary operator. This is compliant -with PEP 8, but this behaviour will cause flake8 to raise -`W503 line break before binary operator` warnings. - In some cases, as determined by PEP 8, _Black_ will enforce an equal amount of whitespace around slice operators. Due to this, Flake8 will raise -`E203 whitespace before ':'` warnings. - -Since both of these warnings are not PEP 8 compliant, Flake8 should be configured to -ignore these warnings via `extend-ignore = E203, W503`. +`E203 whitespace before ':'` warnings. Since this warning is not PEP 8 compliant, Flake8 +should be configured to ignore it via `extend-ignore = E203`. Also, as like with isort, flake8 should be configured to allow lines up to the length limit of `88`, _Black_'s default. This explains `max-line-length = 88`. @@ -173,7 +167,7 @@ limit of `88`, _Black_'s default. This explains `max-line-length = 88`. ```ini [flake8] max-line-length = 88 -extend-ignore = E203, W503 +extend-ignore = E203 ```
@@ -184,7 +178,7 @@ extend-ignore = E203, W503 ```cfg [flake8] max-line-length = 88 -extend-ignore = E203, W503 +extend-ignore = E203 ```
@@ -195,7 +189,7 @@ extend-ignore = E203, W503 ```ini [flake8] max-line-length = 88 -extend-ignore = E203, W503 +extend-ignore = E203 ```
diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 09d58307a05..735e9c015d7 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -199,12 +199,12 @@ You'd do it like this: max-line-length = 80 ... select = C,E,F,W,B,B950 -ignore = E203, E501, W503 +extend-ignore = E203, E501 ``` You'll find _Black_'s own .flake8 config file is configured like this. Explanation of -why W503 and E203 are disabled can be found further in this documentation. And if you're -curious about the reasoning behind B950, +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". @@ -309,10 +309,6 @@ 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. -This behaviour may raise `W503 line break before binary operator` warnings in style -guide enforcement tools like Flake8. Since `W503` is not PEP 8 compliant, you should -tell Flake8 to ignore these warnings. - ### Slices PEP 8 From 1d2d7264ec7c448744b771910cc972da03b1cb80 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 31 Aug 2020 17:20:05 -0400 Subject: [PATCH 056/680] Fix incorrect space before colon in if/while stmts (#1655) * Fix incorrect space before colon in if/while stmts Previously Black would format this code ``` if (foo := True): print(foo) ``` as ``` if (foo := True) : print(foo) ``` adding an incorrect space after the RPAR. Buggy code in the normalize_invisible_parens function caused the colon to be wrapped in invisible parentheses. The LPAR of that pair was then prefixed with a single space at the request of the whitespace function. This commit fixes the accidental skipping of a pre-condition check which must return True before parenthesis normalization of a specific child Leaf or Node can happen. The pre-condition check being skipped was why the colon was wrapped in invisible parentheses. * Add an entry in CHANGES.md --- CHANGES.md | 3 +++ docs/change_log.md | 5 +++++ src/black/__init__.py | 4 ++-- tests/data/pep_572.py | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1c53604d4d5..7e356f1f29c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,9 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer adds an incorrect space after a parenthesized assignment expression + in if/while statements (#1655) + - fixed a crash when PWD=/ on POSIX (#1631) ### 20.8b1 diff --git a/docs/change_log.md b/docs/change_log.md index b7337166659..cc5015f873c 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -9,6 +9,11 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer adds an incorrect space after a parenthesized assignment expression + in if/while statements (#1655) + +- fixed a crash when PWD=/ on POSIX (#1631) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 048e771ce96..64a18655905 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5190,9 +5190,9 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: if check_lpar: if is_walrus_assignment(child): - continue + pass - if child.type == syms.atom: + elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom(child, parent=node): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): diff --git a/tests/data/pep_572.py b/tests/data/pep_572.py index 9e429f913ce..637b3bb38c6 100644 --- a/tests/data/pep_572.py +++ b/tests/data/pep_572.py @@ -2,6 +2,8 @@ (a := a) if (match := pattern.search(data)) is None: pass +if (match := pattern.search(data)): + pass [y := f(x), y ** 2, y ** 3] filtered_data = [y for x in data if (y := f(x)) is None] (y := f(x)) @@ -41,3 +43,5 @@ def foo(answer: (p := 42) = 5): while x := f(x): pass +while (x := f(x)): + pass From 1af648d0c14342021bb8c4220fb8adcd7362d45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 1 Sep 2020 13:18:46 +0200 Subject: [PATCH 057/680] Mention optional invalid W503 warning in pycodestyle --- docs/compatible_configs.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index 82f13932b0b..990820a6771 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -156,6 +156,13 @@ whitespace around slice operators. Due to this, Flake8 will raise `E203 whitespace before ':'` warnings. Since this warning is not PEP 8 compliant, Flake8 should be configured to ignore it via `extend-ignore = E203`. +When breaking a line, _Black_ will break it before a binary operator. This is compliant +with PEP 8 as of +[April 2016](https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b#diff-64ec08cc46db7540f18f2af46037f599). +There's a disabled-by-default warning in Flake8 which goes against this PEP 8 +recommendation called `W503 line break before binary operator`. It should not be enabled +in your configuration. + Also, as like with isort, flake8 should be configured to allow lines up to the length limit of `88`, _Black_'s default. This explains `max-line-length = 88`. From 6b935a34d08c0b18d858e7da5093dc69a29c55f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 1 Sep 2020 13:24:31 +0200 Subject: [PATCH 058/680] Clarify current trailing comma behavior in the docs --- docs/the_black_code_style.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 735e9c015d7..19464ba482a 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -244,16 +244,6 @@ required due to an inner function starting immediately after. _Black_ will add trailing commas to expressions that are split by comma where each element is on its own line. This includes function signatures. -Unnecessary trailing commas are removed if an expression fits in one line. This makes it -1% more likely that your line won't exceed the allotted line length limit. Moreover, in -this scenario, if you added another argument to your call, you'd probably fit it in the -same line anyway. That doesn't make diffs any larger. - -One exception to removing trailing commas is tuple expressions with just one element. In -this case _Black_ won't touch the single trailing comma as this would unexpectedly -change the underlying data type. Note that this is also the case when commas are used -while indexing. This is a tuple in disguise: `numpy_array[3, ]`. - One exception to adding trailing commas is function signatures containing `*`, `*args`, or `**kwargs`. In this case a trailing comma is only safe to use on Python 3.6. _Black_ will detect if your file is already 3.6+ only and use trailing commas in this situation. @@ -262,6 +252,10 @@ in function signatures that have stars in them. In other words, if you'd like a comma in this situation and _Black_ didn't recognize it was safe to do so, put it there manually and _Black_ will keep it. +A pre-existing trailing comma informs _Black_ to always explode contents of the current +bracket pair into one item per line. Read more about this in the +[Pragmatism](#pragmatism) section below. + ### Strings _Black_ prefers double quotes (`"` and `"""`) over single quotes (`'` and `'''`). It From 25c1b6dff7264278c1ca1649f321a3bd8df293b1 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 4 Sep 2020 17:20:55 -0400 Subject: [PATCH 059/680] Update primer.json to reflect Black's adoption (#1674) - tox recently adopted Black https://github.com/tox-dev/tox/commit/a7903508fa07068b327e15cfdbf8ea330ab78765 - attrs already adopted Black but they updated to 20.08b1 + did a format pass and removed some trailing commas https://github.com/python-attrs/attrs/commit/f680c5b83e65413eeb684c68ece60198015058c3 --- src/black_primer/primer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 546f47782cd..6371d44ce1f 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -10,7 +10,7 @@ }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] @@ -98,7 +98,7 @@ }, "tox": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/tox-dev/tox.git", "long_checkout": false, "py_versions": ["all"] From 17908718338e6ba10d01f3b484ed0fe9542b8169 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 5 Sep 2020 19:38:43 +0300 Subject: [PATCH 060/680] Test primer on Pillow (#1679) --- src/black_primer/primer.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 6371d44ce1f..6086c4c813a 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -61,6 +61,13 @@ "long_checkout": false, "py_versions": ["all"] }, + "pillow": { + "cli_arguments": [], + "expect_formatting_changes": false, + "git_clone_url": "https://github.com/python-pillow/Pillow.git", + "long_checkout": false, + "py_versions": ["all"] + }, "poetry": { "cli_arguments": [], "expect_formatting_changes": true, From 6b5753a41781ef7a8746aeea52c26e07f4b43c27 Mon Sep 17 00:00:00 2001 From: Tom Saunders Date: Sat, 5 Sep 2020 20:15:28 +0100 Subject: [PATCH 061/680] Handle .COLOR_DIFF in the same way as .DIFF (#1673) --- CHANGES.md | 2 ++ src/black/__init__.py | 6 ++--- tests/test_black.py | 53 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7e356f1f29c..52c8016a257 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ - fixed a crash when PWD=/ on POSIX (#1631) +- Prevent coloured diff output being interleaved with multiple files (#1673) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 64a18655905..f7e7603321a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -661,7 +661,7 @@ def reformat_one( changed = Changed.YES else: cache: Cache = {} - if write_back != WriteBack.DIFF: + if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): cache = read_cache(mode) res_src = src.resolve() if res_src in cache and cache[res_src] == get_cache_info(res_src): @@ -735,7 +735,7 @@ async def schedule_formatting( :func:`format_file_in_place`. """ cache: Cache = {} - if write_back != WriteBack.DIFF: + 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): @@ -746,7 +746,7 @@ async def schedule_formatting( cancelled = [] sources_to_cache = [] lock = None - if write_back == WriteBack.DIFF: + 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() diff --git a/tests/test_black.py b/tests/test_black.py index bc80c8fca8b..edcf7208b46 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import multiprocessing import asyncio import logging from concurrent.futures import ThreadPoolExecutor @@ -1395,9 +1396,55 @@ def test_no_cache_when_writeback_diff(self) -> None: src = (workspace / "test.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - self.invokeBlack([str(src), "--diff"]) - cache_file = black.get_cache_file(mode) - self.assertFalse(cache_file.exists()) + with patch("black.read_cache") as read_cache, patch( + "black.write_cache" + ) as write_cache: + self.invokeBlack([str(src), "--diff"]) + cache_file = black.get_cache_file(mode) + self.assertFalse(cache_file.exists()) + write_cache.assert_not_called() + read_cache.assert_not_called() + + def test_no_cache_when_writeback_color_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", "--color"]) + cache_file = black.get_cache_file(mode) + self.assertFalse(cache_file.exists()) + write_cache.assert_not_called() + read_cache.assert_not_called() + + @event_loop() + def test_output_locking_when_writeback_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", 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() + + @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 From e3ccabb23c5dc5495bd8f96b5c90c1db6a350d6d Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sat, 5 Sep 2020 20:24:00 -0400 Subject: [PATCH 062/680] Fix unstable formatting on string split + % formatting (#1680) Fixes #1595 --- src/black/__init__.py | 2 +- src/blib2to3/pgen2/pgen.py | 3 +-- tests/data/long_strings.py | 6 ++---- tests/data/long_strings__regression.py | 21 +++++++++++++++++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index f7e7603321a..eed059e2837 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2668,9 +2668,9 @@ def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: transformers = [ string_merge, string_paren_strip, + string_split, delimiter_split, standalone_comment_split, - string_split, string_paren_wrap, rhs, ] diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index 13ec51d1878..a685145933c 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -168,8 +168,7 @@ def calcfirst(self, name: Text) -> None: if symbol in inverse: raise ValueError( "rule %s is ambiguous; %s is in the first sets of %s as well" - " as %s" - % (name, symbol, label, inverse[symbol]) + " as %s" % (name, symbol, label, inverse[symbol]) ) inverse[symbol] = label self.first[name] = totalset diff --git a/tests/data/long_strings.py b/tests/data/long_strings.py index e1ed90f22de..151396b5239 100644 --- a/tests/data/long_strings.py +++ b/tests/data/long_strings.py @@ -380,8 +380,7 @@ def foo(): old_fmt_string1 = ( "While we are on the topic of %s, we should also note that old-style formatting" - " must also be preserved, since some %s still uses it." - % ("formatting", "code") + " must also be preserved, since some %s still uses it." % ("formatting", "code") ) old_fmt_string2 = "This is a %s %s %s %s" % ( @@ -448,8 +447,7 @@ def foo(): assert some_type_of_boolean_expression, ( "Followed by a really really really long string that is used to provide context to" - " the AssertionError exception, which uses dynamic string %s." - % "formatting" + " the AssertionError exception, which uses dynamic string %s." % "formatting" ) assert some_type_of_boolean_expression, ( diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 044bb4a5deb..33bf14cfaa3 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -310,6 +310,13 @@ def who(self): passenger_association=passenger_association, ) +if __name__ == "__main__": + for i in range(4, 8): + cmd = ( + r"for pid in $(ps aux | grep paster | grep -v grep | grep '\-%d' | awk '{print $2}'); do kill $pid; done" + % (i) + ) + # output @@ -435,14 +442,12 @@ def foo(): func_call_where_string_arg_has_old_fmt_and_bad_parens( "A long string with {}. This string is so long that it is ridiculous. It can't fit" - " on one line at alllll." - % "formatting", + " on one line at alllll." % "formatting", ) func_call_where_string_arg_has_old_fmt_and_bad_parens( "A long string with {}. This {} is so long that it is ridiculous. It can't fit on" - " one line at alllll." - % ("formatting", "string"), + " one line at alllll." % ("formatting", "string"), ) @@ -702,3 +707,11 @@ def who(self): passenger_association=passenger_association, ) ) + + +if __name__ == "__main__": + for i in range(4, 8): + cmd = ( + r"for pid in $(ps aux | grep paster | grep -v grep | grep '\-%d' | awk" + r" '{print $2}'); do kill $pid; done" % (i) + ) From 7bca930ca3d84bbd01e98937b6b8a493d0254c7c Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 6 Sep 2020 11:02:57 -0400 Subject: [PATCH 063/680] Fix crash on concatenated string + comment (fixes #1596) (#1677) Co-authored-by: Jelle Zijlstra --- src/black/__init__.py | 28 +++++++++++++++++++++----- tests/data/long_strings__regression.py | 27 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index eed059e2837..ed5256eefe1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2889,11 +2889,8 @@ class StringMerger(CustomSplitMapMixin, StringTransformer): """StringTransformer that merges strings together. Requirements: - (A) The line contains adjacent strings such that at most one substring - has inline comments AND none of those inline comments are pragmas AND - the set of all substring prefixes is either of length 1 or equal to - {"", "f"} AND none of the substrings are raw strings (i.e. are prefixed - with 'r'). + (A) The line contains adjacent strings such that ALL of the validation checks + listed in StringMerger.__validate_msg(...)'s docstring pass. OR (B) The line contains a string which uses line continuation backslashes. @@ -3142,6 +3139,7 @@ def __validate_msg(line: Line, string_idx: int) -> TResult[None]: * Ok(None), if ALL validation checks (listed below) pass. OR * Err(CannotTransform), if any of the following are true: + - The target string group does not contain ANY stand-alone comments. - The target string is not in a string group (i.e. it has no adjacent strings). - The string group has more than one inline comment. @@ -3150,6 +3148,26 @@ def __validate_msg(line: Line, string_idx: int) -> TResult[None]: length greater than one and is not equal to {"", "f"}. - The string group consists of raw strings. """ + # We first check for "inner" stand-alone comments (i.e. stand-alone + # comments that have a string leaf before them AND after them). + for inc in [1, -1]: + i = string_idx + found_sa_comment = False + is_valid_index = is_valid_index_factory(line.leaves) + while is_valid_index(i) and line.leaves[i].type in [ + token.STRING, + STANDALONE_COMMENT, + ]: + if line.leaves[i].type == STANDALONE_COMMENT: + found_sa_comment = True + elif found_sa_comment: + return TErr( + "StringMerger does NOT merge string groups which contain " + "stand-alone comments." + ) + + i += inc + num_of_inline_string_comments = 0 set_of_prefixes = set() num_of_strings = 0 diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 33bf14cfaa3..745a636cdcf 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -310,6 +310,19 @@ def who(self): passenger_association=passenger_association, ) +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): cmd = ( @@ -709,6 +722,20 @@ 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") && ' + ) + ) + ] +) + if __name__ == "__main__": for i in range(4, 8): cmd = ( From 6284953d07060804dcdeacf4626b76aed7a20683 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 6 Sep 2020 12:15:40 -0400 Subject: [PATCH 064/680] Fix crash on assert and parenthesized % format (fixes #1597, fixes #1605) (#1681) --- src/black/__init__.py | 27 ++++++++++++++--- tests/data/long_strings__regression.py | 42 ++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index ed5256eefe1..3753d5fca8d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -112,6 +112,10 @@ class InvalidInput(ValueError): """Raised when input source code fails all parse attempts.""" +class BracketMatchError(KeyError): + """Raised when an opening bracket is unable to be matched to a closing bracket.""" + + T = TypeVar("T") E = TypeVar("E", bound=Exception) @@ -1308,7 +1312,13 @@ def mark(self, leaf: Leaf) -> None: self.maybe_decrement_after_lambda_arguments(leaf) if leaf.type in CLOSING_BRACKETS: self.depth -= 1 - opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) + try: + opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) + except KeyError as e: + raise BracketMatchError( + "Unable to match a closing bracket to the following opening" + f" bracket: {leaf}" + ) from e leaf.opening_bracket = opening_bracket if not leaf.value: self.invisible.append(leaf) @@ -3324,10 +3334,17 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: yield TErr( "Will not strip parentheses which have comments attached to them." ) + return new_line = line.clone() new_line.comments = line.comments.copy() - append_leaves(new_line, line, LL[: string_idx - 1]) + 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) string_leaf = Leaf(token.STRING, LL[string_idx].value) LL[string_idx - 1].remove() @@ -4598,7 +4615,9 @@ def line_to_string(line: Line) -> str: return str(line).strip("\n") -def append_leaves(new_line: Line, old_line: Line, leaves: List[Leaf]) -> None: +def append_leaves( + new_line: Line, old_line: Line, leaves: List[Leaf], preformatted: bool = False +) -> None: """ Append leaves (taken from @old_line) to @new_line, making sure to fix the underlying Node structure where appropriate. @@ -4614,7 +4633,7 @@ def append_leaves(new_line: Line, old_line: Line, leaves: List[Leaf]) -> None: for old_leaf in leaves: new_leaf = Leaf(old_leaf.type, old_leaf.value) replace_child(old_leaf, new_leaf) - new_line.append(new_leaf) + new_line.append(new_leaf, preformatted=preformatted) for comment_leaf in old_line.comments_after(old_leaf): new_line.append(comment_leaf, preformatted=True) diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 745a636cdcf..1c82613d410 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -330,6 +330,25 @@ def who(self): % (i) ) +def A(): + def B(): + def C(): + def D(): + def E(): + def F(): + def G(): + assert ( + c_float(val[0][0] / val[0][1]).value + == c_float(value[0][0] / value[0][1]).value + ), "%s didn't roundtrip" % tag + +class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxxxxxxxxx): + def xxxxxxx_xxxxxx(xxxx): + assert xxxxxxx_xxxx in [ + x.xxxxx.xxxxxx.xxxxx.xxxxxx, + x.xxxxx.xxxxxx.xxxxx.xxxx, + ], ("xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx) + # output @@ -742,3 +761,26 @@ def who(self): r"for pid in $(ps aux | grep paster | grep -v grep | grep '\-%d' | awk" r" '{print $2}'); do kill $pid; done" % (i) ) + + +def A(): + def B(): + def C(): + def D(): + def E(): + def F(): + def G(): + assert ( + c_float(val[0][0] / val[0][1]).value + == c_float(value[0][0] / value[0][1]).value + ), "%s didn't roundtrip" % tag + + +class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxxxxxxxxx): + def xxxxxxx_xxxxxx(xxxx): + assert xxxxxxx_xxxx in [ + x.xxxxx.xxxxxx.xxxxx.xxxxxx, + x.xxxxx.xxxxxx.xxxxx.xxxx, + ], ( + "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx + ) From 1e8916f450505bfe58649c1ff8c0a3708de47daa Mon Sep 17 00:00:00 2001 From: Michael Wilkinson Date: Tue, 8 Sep 2020 12:42:57 -0400 Subject: [PATCH 065/680] Add link to conda-forge integration (#1687) * Add link to conda-forge integration resolves #1686 * README: keep PyPI tags together Co-authored-by: Hugo van Kemenade --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 20f6fa420b2..942e85444d3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ License: MIT PyPI Downloads +conda-forge Code style: black

From cd055efd7d999421984e8b63afe53b5f9854deb9 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Thu, 10 Sep 2020 12:24:01 -0400 Subject: [PATCH 066/680] Fix unstable subscript assignment string wrapping (#1678) Fixes #1598 --- src/black/__init__.py | 7 +++++-- tests/data/long_strings__regression.py | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 3753d5fca8d..bfb77126218 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3507,9 +3507,12 @@ def __get_max_string_length(self, line: Line, string_idx: int) -> int: # WMA4 a single space. offset += 1 - # WMA4 the lengths of any leaves that came before that space. - for leaf in LL[: p_idx + 1]: + # WMA4 the lengths of any leaves that came before that space, + # but after any closing bracket before that space. + for leaf in reversed(LL[: p_idx + 1]): offset += len(str(leaf)) + if leaf.type in CLOSING_BRACKETS: + break if is_valid_index(string_idx + 1): N = LL[string_idx + 1] diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 1c82613d410..8290a4cbc1c 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -349,6 +349,10 @@ def xxxxxxx_xxxxxx(xxxx): x.xxxxx.xxxxxx.xxxxx.xxxx, ], ("xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx) +value.__dict__[ + key +] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee + # output @@ -784,3 +788,8 @@ def xxxxxxx_xxxxxx(xxxx): ], ( "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx ) + + +value.__dict__[ + key +] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee From ecc1f17ee57fd1d6c29e47194b1025721f99455b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 10 Sep 2020 22:52:23 +0300 Subject: [PATCH 067/680] Virtualenv is now formatted with newest Black https://github.com/pypa/virtualenv/pull/1939 (#1695) --- 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 6086c4c813a..8a90192f27d 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -112,7 +112,7 @@ }, "virtualenv": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pypa/virtualenv.git", "long_checkout": false, "py_versions": ["all"] From c0a8e4224360a6917dcb5d889b08d5fdcfaf7c2d Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 10 Sep 2020 16:21:37 -0400 Subject: [PATCH 068/680] Fix empty line handling when formatting typing stubs (#1646) Black used to erroneously remove all empty lines between non-function code and decorators when formatting typing stubs. Now a single empty line is enforced. I chose for putting empty lines around decorated classes that have empty bodies since removing empty lines around such classes would cause a formatting issue that seems to be impossible to fix. For example: ``` class A: ... @some_decorator class B: ... class C: ... class D: ... @some_other_decorator def foo(): -> None: ... ``` It is easy to enforce no empty lines between class A, B, and C. Just return 0, 0 for a line that is a decorator and precedes an stub class. Fortunately before this commit, empty lines after that class would be removed already. Now let's look at the empty line between class D and function foo. In this case, there should be an empty line there since it's class code next to function code. The problem is that when deciding to add X empty lines before a decorator, you can't tell whether it's before a class or a function. If the decorator is before a function, then an empty line is needed, while no empty lines are needed when the decorator is before a class. So even though I personally prefer no empty lines around decorated classes, I had to go the other way surrounding decorated classes with empty lines. Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 ++ docs/change_log.md | 3 ++ src/black/__init__.py | 11 +++++-- tests/data/force_pyi.py | 67 ++++++++++++++++++++++++++++++++++++++--- tests/test_black.py | 7 +++-- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 52c8016a257..59d9320639b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,9 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer removes all empty lines between non-function code and decorators + when formatting typing stubs. Now `Black` enforces a single empty line. (#1646) + - `Black` no longer adds an incorrect space after a parenthesized assignment expression in if/while statements (#1655) diff --git a/docs/change_log.md b/docs/change_log.md index cc5015f873c..e183ca545b6 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -9,6 +9,9 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer removes all empty lines between non-function code and decorators + when formatting typing stubs. Now `Black` enforces a single empty line. (#1646) + - `Black` no longer adds an incorrect space after a parenthesized assignment expression in if/while statements (#1655) diff --git a/src/black/__init__.py b/src/black/__init__.py index bfb77126218..9e18a7d904b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1834,6 +1834,10 @@ 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: + # Insert an empty line after a decorated stub class + return 0, 1 + return 0, 0 if self.previous_line.depth < current_line.depth and ( @@ -1857,8 +1861,11 @@ def _maybe_empty_lines_for_class_or_def( newlines = 0 else: newlines = 1 - elif current_line.is_def and not self.previous_line.is_def: - # Blank line between a block of functions and a block of non-functions + 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 else: newlines = 0 diff --git a/tests/data/force_pyi.py b/tests/data/force_pyi.py index 25246c22ca7..07ed93c6879 100644 --- a/tests/data/force_pyi.py +++ b/tests/data/force_pyi.py @@ -1,6 +1,65 @@ -def f(): ... +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... -def g(): ... # output -def f(): ... -def g(): ... + +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/tests/test_black.py b/tests/test_black.py index edcf7208b46..e928dc04984 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1574,7 +1574,6 @@ def test_tricky_unicode_symbols(self) -> None: black.assert_stable(source, actual, DEFAULT_MODE) def test_single_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: @@ -1587,9 +1586,11 @@ def test_single_file_force_pyi(self) -> None: # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) self.assertIn(path, pyi_cache) - normal_cache = black.read_cache(reg_mode) + normal_cache = black.read_cache(DEFAULT_MODE) self.assertNotIn(path, normal_cache) - self.assertEqual(actual, expected) + 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: From 811decd7f10fb2fb3ae343b9d9d0a3ae53b86a53 Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Sun, 13 Sep 2020 17:59:18 +0200 Subject: [PATCH 069/680] Fix typo in docstring (#1700) Added a missing preposition --- 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 9e18a7d904b..ffaafe23f9e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -925,7 +925,7 @@ def format_stdin_to_stdout( def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: - """Reformat contents a file and return new contents. + """Reformat contents of a file and return new contents. If `fast` is False, additionally confirm that the reformatted code is valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. From 6dddbd72414061cde9dd8ee72eac373b7fcf8b54 Mon Sep 17 00:00:00 2001 From: QuentinSoubeyran <45202794+QuentinSoubeyran@users.noreply.github.com> Date: Sat, 19 Sep 2020 20:33:10 +0200 Subject: [PATCH 070/680] PEP 614 support (#1717) --- CHANGES.md | 2 + Pipfile | 2 +- Pipfile.lock | 297 +++++++++++++++++++++++++-------------- src/black/__init__.py | 70 ++++++++- src/blib2to3/Grammar.txt | 2 +- tests/data/decorators.py | 176 +++++++++++++++++++++++ tests/data/python39.py | 37 +++++ tests/test_black.py | 61 +++++++- 8 files changed, 530 insertions(+), 117 deletions(-) create mode 100644 tests/data/decorators.py create mode 100644 tests/data/python39.py diff --git a/CHANGES.md b/CHANGES.md index 59d9320639b..7ad3482d07a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Prevent coloured diff output being interleaved with multiple files (#1673) +- Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) + ### 20.8b1 #### _Packaging_ diff --git a/Pipfile b/Pipfile index 44f57f6773c..1ced1ed096b 100644 --- a/Pipfile +++ b/Pipfile @@ -29,7 +29,7 @@ mypy_extensions = ">=0.4.3" pathspec = ">=0.6" regex = ">=2020.1.8" toml = ">=0.10.1" -typed-ast = "==1.4.0" +typed-ast = "==1.4.1" typing_extensions = ">=3.7.4" black = {editable = true,extras = ["d"],path = "."} dataclasses = {"python_version <" = "3.7","version >" = "0.6"} diff --git a/Pipfile.lock b/Pipfile.lock index 32b8012ff0e..e09d69ea83d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "61d09a6b8a8c310becd5e108ed08e0eeae50c7323c08c8040367abded0cb1031" + "sha256": "46390803c9b9e1b77a1b4a29de602d864dea188488d3aee6361030c91529611c" }, "pipfile-spec": 6, "requires": {}, @@ -58,11 +58,11 @@ }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "version": "==20.2.0" }, "black": { "editable": true, @@ -186,39 +186,40 @@ }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c" ], "index": "pypi", "version": "==0.10.1" }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "index": "pypi", - "version": "==1.4.0" + "version": "==1.4.1" }, "typing-extensions": { "hashes": [ @@ -305,11 +306,11 @@ }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "version": "==20.2.0" }, "babel": { "hashes": [ @@ -328,11 +329,11 @@ }, "bleach": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1.5" + "version": "==3.2.1" }, "certifi": { "hashes": [ @@ -341,6 +342,39 @@ ], "version": "==2020.6.20" }, + "cffi": { + "hashes": [ + "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", + "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", + "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", + "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", + "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", + "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", + "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", + "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", + "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", + "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", + "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", + "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", + "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", + "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", + "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", + "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", + "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", + "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", + "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", + "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", + "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", + "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", + "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", + "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", + "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", + "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", + "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", + "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" + ], + "version": "==1.14.2" + }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -381,43 +415,71 @@ }, "coverage": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" - ], - "index": "pypi", - "version": "==5.2.1" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "index": "pypi", + "version": "==5.3" + }, + "cryptography": { + "hashes": [ + "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a", + "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed", + "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36", + "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08", + "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237", + "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618", + "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f", + "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695", + "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c", + "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10", + "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c", + "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1", + "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e", + "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f", + "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791", + "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0", + "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af", + "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8", + "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761", + "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716", + "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32", + "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.1" }, "distlib": { "hashes": [ @@ -468,11 +530,11 @@ }, "identify": { "hashes": [ - "sha256:9f5fcf22b665eaece583bd395b103c2769772a0f646ffabb5b1f155901b07de2", - "sha256:b1aa2e05863dc80242610d46a7b49105e2eafe00ef0c8ff311c1828680760c76" + "sha256:c770074ae1f19e08aadbda1c886bc6d0cb55ffdc503a8c0fe8699af2fc9664ae", + "sha256:d02d004568c5a01261839a05e91705e3e9f5c57a3551648f9b3fb2b9c62c0f62" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.29" + "version": "==1.5.3" }, "idna": { "hashes": [ @@ -490,6 +552,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, + "jeepney": { + "hashes": [ + "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", + "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.4.3" + }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", @@ -500,11 +570,11 @@ }, "keyring": { "hashes": [ - "sha256:182f94fc0381546489e3e4d90384a8c1d43cc09ffe2eb4a826e7312df6e1be7c", - "sha256:cd4d486803d55bdb13e2d453eb61dbbc984773e4f2b98a455aa85b1f4bc421e4" + "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", + "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], "markers": "python_version >= '3.6'", - "version": "==21.3.1" + "version": "==21.4.0" }, "markupsafe": { "hashes": [ @@ -649,6 +719,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -659,11 +737,11 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], "markers": "python_version >= '3.5'", - "version": "==2.6.1" + "version": "==2.7.1" }, "pyparsing": { "hashes": [ @@ -761,6 +839,14 @@ ], "version": "==1.4.0" }, + "secretstorage": { + "hashes": [ + "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", + "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.1.2" + }, "setuptools-scm": { "hashes": [ "sha256:09c659d1d6680811c43f476a33c6d3d9872416580786e96bd29ea03e6a818e41", @@ -848,21 +934,21 @@ }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c" ], "index": "pypi", "version": "==0.10.1" }, "tqdm": { "hashes": [ - "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", - "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" + "sha256:8f3c5815e3b5e20bc40463fa6b42a352178859692a68ffaa469706e6d38342a5", + "sha256:faf9c671bd3fad5ebaeee366949d969dca2b2be32c872a7092a1e1a9048d105b" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.48.2" + "version": "==4.49.0" }, "twine": { "hashes": [ @@ -874,29 +960,30 @@ }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "index": "pypi", - "version": "==1.4.0" + "version": "==1.4.1" }, "typing-extensions": { "hashes": [ diff --git a/src/black/__init__.py b/src/black/__init__.py index ffaafe23f9e..c1db8e6ef64 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -178,14 +178,12 @@ class TargetVersion(Enum): PY36 = 6 PY37 = 7 PY38 = 8 + PY39 = 9 def is_python2(self) -> bool: return self is TargetVersion.PY27 -PY36_VERSIONS = {TargetVersion.PY36, TargetVersion.PY37, TargetVersion.PY38} - - class Feature(Enum): # All string literals are unicode UNICODE_LITERALS = 1 @@ -199,6 +197,7 @@ class Feature(Enum): ASYNC_KEYWORDS = 7 ASSIGNMENT_EXPRESSIONS = 8 POS_ONLY_ARGUMENTS = 9 + RELAXED_DECORATORS = 10 FORCE_OPTIONAL_PARENTHESES = 50 @@ -237,6 +236,17 @@ class Feature(Enum): Feature.ASSIGNMENT_EXPRESSIONS, Feature.POS_ONLY_ARGUMENTS, }, + TargetVersion.PY39: { + 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, + }, } @@ -2184,6 +2194,9 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 ): # 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 elif prev.type in OPENING_BRACKETS: return NO @@ -5499,6 +5512,49 @@ def is_walrus_assignment(node: LN) -> bool: return inner is not None and inner.type == syms.namedexpr_test +def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: + """Return True iff `node` is a trailer valid in a simple decorator""" + return node.type == syms.trailer and ( + ( + len(node.children) == 2 + and node.children[0].type == token.DOT + and node.children[1].type == token.NAME + ) + # last trailer can be arguments + or ( + last + and len(node.children) == 3 + and node.children[0].type == token.LPAR + # and node.children[1].type == syms.argument + and node.children[2].type == token.RPAR + ) + ) + + +def is_simple_decorator_expression(node: LN) -> bool: + """Return True iff `node` could be a 'dotted name' decorator + + This function takes the node of the 'namedexpr_test' of the new decorator + grammar and test if it would be valid under the old decorator grammar. + + The old grammar was: decorator: @ dotted_name [arguments] NEWLINE + The new grammar is : decorator: @ namedexpr_test NEWLINE + """ + if node.type == token.NAME: + return True + if node.type == syms.power: + if node.children: + return ( + node.children[0].type == token.NAME + and all(map(is_simple_decorator_trailer, node.children[1:-1])) + and ( + len(node.children) < 2 + or is_simple_decorator_trailer(node.children[-1], last=True) + ) + ) + return False + + def is_yield(node: LN) -> bool: """Return True if `node` holds a `yield` or `yield from` expression.""" if node.type == syms.yield_expr: @@ -5684,6 +5740,8 @@ def get_features_used(node: Node) -> Set[Feature]: - underscores in numeric literals; - trailing commas after * or ** in function signatures and calls; - positional only arguments in function signatures and lambdas; + - assignment expression; + - relaxed decorator syntax; """ features: Set[Feature] = set() for n in node.pre_order(): @@ -5703,6 +5761,12 @@ def get_features_used(node: Node) -> Set[Feature]: elif n.type == token.COLONEQUAL: features.add(Feature.ASSIGNMENT_EXPRESSIONS) + elif n.type == syms.decorator: + if len(n.children) > 1 and not is_simple_decorator_expression( + n.children[1] + ): + features.add(Feature.RELAXED_DECORATORS) + elif ( n.type in {syms.typedargslist, syms.arglist} and n.children diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index f14e2b516bd..eafaee84cb3 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -12,7 +12,7 @@ file_input: (NEWLINE | stmt)* ENDMARKER single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE eval_input: testlist NEWLINE* ENDMARKER -decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE +decorator: '@' namedexpr_test NEWLINE decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: ASYNC funcdef diff --git a/tests/data/decorators.py b/tests/data/decorators.py new file mode 100644 index 00000000000..acfad51fcb8 --- /dev/null +++ b/tests/data/decorators.py @@ -0,0 +1,176 @@ +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... + +# output + +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... \ No newline at end of file diff --git a/tests/data/python39.py b/tests/data/python39.py new file mode 100644 index 00000000000..ae67c2257eb --- /dev/null +++ b/tests/data/python39.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +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(): + ... + +# output + + +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +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 diff --git a/tests/test_black.py b/tests/test_black.py index e928dc04984..e17f43b88d4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -59,9 +59,13 @@ PROJECT_ROOT = THIS_DIR.parent DETERMINISTIC_HEADER = "[Deterministic header]" EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" -PY36_ARGS = [ - f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS -] +PY36_VERSIONS = { + TargetVersion.PY36, + TargetVersion.PY37, + TargetVersion.PY38, + TargetVersion.PY39, +} +PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS] T = TypeVar("T") R = TypeVar("R") @@ -705,7 +709,7 @@ def test_string_prefixes(self) -> None: @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=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -714,7 +718,7 @@ def test_numeric_literals(self) -> None: @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=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -800,6 +804,16 @@ def test_python38(self) -> None: 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) + @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff(self) -> None: source, expected = read_data("fmtonoff") @@ -1212,6 +1226,39 @@ def test_lib2to3_parse(self) -> None: 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 + # 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") + # skip explanation comments at the top of the file + for simple_test in simples.split("##")[1:]: + node = black.lib2to3_parse(simple_test) + decorator = str(node.children[0].children[0]).strip() + self.assertNotIn( + Feature.RELAXED_DECORATORS, + black.get_features_used(node), + msg=( + f"decorator '{decorator}' follows python<=3.8 syntax" + "but is detected as 3.9+" + # f"The full node is\n{node!r}" + ), + ) + # skip the '# output' comment at the top of the output part + for relaxed_test in relaxed.split("##")[1:]: + node = black.lib2to3_parse(relaxed_test) + decorator = str(node.children[0].children[0]).strip() + self.assertIn( + Feature.RELAXED_DECORATORS, + black.get_features_used(node), + msg=( + f"decorator '{decorator}' uses python3.9+ syntax" + "but is detected as python<=3.8" + # f"The full node is\n{node!r}" + ), + ) + def test_get_features_used(self) -> None: node = black.lib2to3_parse("def f(*, arg): ...\n") self.assertEqual(black.get_features_used(node), set()) @@ -1628,7 +1675,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=black.PY36_VERSIONS) + py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() @@ -1647,7 +1694,7 @@ def test_single_file_force_py36(self) -> None: @event_loop() def test_multi_file_force_py36(self) -> None: reg_mode = DEFAULT_MODE - py36_mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) + py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: paths = [ From bc138d12630ec38cb1b11b2fb2ca8762ac2ae7a4 Mon Sep 17 00:00:00 2001 From: Vipul Date: Sun, 27 Sep 2020 05:54:21 +0000 Subject: [PATCH 071/680] End 'force-exclude' help message with a period (#1727) It would be nice, if like other options help message, force-exclude's help message also ends with a period punctuation mark. --- README.md | 2 +- docs/installation_and_usage.md | 2 +- src/black/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 942e85444d3..aef850f1415 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Options: --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even - when they are passed explicitly as arguments + when they are passed explicitly as arguments. -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index cc0269198a2..7d37f59264b 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -88,7 +88,7 @@ Options: --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even - when they are passed explicitly as arguments + when they are passed explicitly as arguments. -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with diff --git a/src/black/__init__.py b/src/black/__init__.py index c1db8e6ef64..dce7a24445a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -454,7 +454,7 @@ def target_version_option_callback( type=str, help=( "Like --exclude, but files and directories matching this regex will be " - "excluded even when they are passed explicitly as arguments" + "excluded even when they are passed explicitly as arguments." ), ) @click.option( From 4d71d74a442ccc6c309a0667147997c1eeb755fd Mon Sep 17 00:00:00 2001 From: Pete Grayson Date: Sun, 27 Sep 2020 21:41:11 +0000 Subject: [PATCH 072/680] Repair colorama wrapping on non-Windows platforms (#1670) * Repair colorama wrapping on non-Windows platforms The wrap_stream_for_windows() function calls colorama.initialise.wrap_stream() function to apply colorama's magic to wrapper to the output stream. Except this wrapper is only applied on Windows platforms that need it, otherwise the original stream is returned as-is. The colorama wrapped stream lacks a detach() method, so a no-op lambda was being assigned to the wrapped stream. The problem is that the no-op lambda was being assigned unconditionally whether or not colorama actually returns a wrapped stream, thus replacing the original TextIOWrapper's detach() method. Replacing the detach() method with a no-op lambda is the root cause of the problem observed in #1664. The solution is to only assign the no-op detach() method if the stream lacks its own detach() method. Repairs #1664 --- CHANGES.md | 2 ++ README.md | 1 + src/black/__init__.py | 30 +++++++++++------------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7ad3482d07a..dfc54224b41 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - fixed a crash when PWD=/ on POSIX (#1631) +- fixed "I/O operation on closed file" when using --diff (#1664) + - Prevent coloured diff output being interleaved with multiple files (#1673) - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) diff --git a/README.md b/README.md index aef850f1415..f17b353b023 100644 --- a/README.md +++ b/README.md @@ -636,6 +636,7 @@ Multiple contributions by: - [Paul Ganssle](mailto:p.ganssle@gmail.com) - [Paul Meinhardt](mailto:mnhrdt@gmail.com) - [Peter Bengtsson](mailto:mail@peterbe.com) +- [Peter Grayson](mailto:pete@jpgrayson.net) - [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) - pmacosta - [Quentin Pradet](mailto:quentin@pradet.me) diff --git a/src/black/__init__.py b/src/black/__init__.py index dce7a24445a..9af58014c51 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -871,30 +871,22 @@ def color_diff(contents: str) -> str: def wrap_stream_for_windows( f: io.TextIOWrapper, -) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32.AnsiToWin32"]: +) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]: """ - Wrap the stream in colorama's wrap_stream so colors are shown on Windows. + Wrap stream with colorama's wrap_stream so colors are shown on Windows. - If `colorama` is not found, then no change is made. If `colorama` does - exist, then it handles the logic to determine whether or not to change - things. + If `colorama` is unavailable, the original stream is returned unmodified. + Otherwise, the `wrap_stream()` function determines whether the stream needs + to be wrapped for a Windows environment and will accordingly either return + an `AnsiToWin32` wrapper or the original stream. """ try: - from colorama import initialise - - # We set `strip=False` so that we can don't have to modify - # test_express_diff_with_color. - f = initialise.wrap_stream( - f, convert=None, strip=False, autoreset=False, wrap=True - ) - - # wrap_stream returns a `colorama.AnsiToWin32.AnsiToWin32` object - # which does not have a `detach()` method. So we fake one. - f.detach = lambda *args, **kwargs: None # type: ignore + from colorama.initialise import wrap_stream except ImportError: - pass - - return f + return f + else: + # Set `strip=False` to avoid needing to modify test_express_diff_with_color. + return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True) def format_stdin_to_stdout( From 82c1f871d07e9c6334b5fb0bf089273a08ffd195 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Sep 2020 05:42:01 +0300 Subject: [PATCH 073/680] Hypothesis is now formatted with Black 20.8b1 (#1729) --- 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 8a90192f27d..5818444e632 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -47,7 +47,7 @@ }, "hypothesis": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] From 172c0a78facd8f938629f216c95507bbfeddfe5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hakan=20=C3=87elik?= Date: Mon, 28 Sep 2020 22:55:35 +0300 Subject: [PATCH 074/680] Fix unnecessary if checks (#1728) --- src/black/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 9af58014c51..24e9d4edaaa 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -859,9 +859,9 @@ def color_diff(contents: str) -> str: for i, line in enumerate(lines): if line.startswith("+++") or line.startswith("---"): line = "\033[1;37m" + line + "\033[0m" # bold white, reset - if line.startswith("@@"): + elif line.startswith("@@"): line = "\033[36m" + line + "\033[0m" # cyan, reset - if line.startswith("+"): + elif line.startswith("+"): line = "\033[32m" + line + "\033[0m" # green, reset elif line.startswith("-"): line = "\033[31m" + line + "\033[0m" # red, reset From 283d999c3f89e2204cbf5a61242665bedd6887ef Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 2 Oct 2020 14:47:57 +0300 Subject: [PATCH 075/680] Primer: pyramid and sqlalchemy are now formatted with latest Black (#1736) --- src/black_primer/primer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 5818444e632..cdc863ca032 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -77,7 +77,7 @@ }, "pyramid": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/Pylons/pyramid.git", "long_checkout": false, "py_versions": ["all"] @@ -98,7 +98,7 @@ }, "sqlalchemy": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", "long_checkout": false, "py_versions": ["all"] From d9884b6d330d56b2d2ea1d24f1bdf36ff433c0e6 Mon Sep 17 00:00:00 2001 From: Cha Gyuseok Date: Wed, 7 Oct 2020 23:45:06 +0900 Subject: [PATCH 076/680] docs: update `used-by` link to proper url (#1745) --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f17b353b023..9c10a63fe29 100644 --- a/README.md +++ b/README.md @@ -212,12 +212,13 @@ know!) ### NOTE: This is a beta product -_Black_ is already [successfully used](#used-by) by many projects, small and big. It -also sports a decent test suite. However, it is still very new. 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. +_Black_ is already [successfully used](https://github.com/psf/black#used-by) by many +projects, small and big. It also sports a decent test suite. However, it is still very +new. 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. Also, as a temporary safety measure, _Black_ will check that the reformatted code still produces a valid AST that is equivalent to the original. This slows it down. If you're From dd2f86ac0a043815821d228b9db036a295be5372 Mon Sep 17 00:00:00 2001 From: Hadi Alqattan Date: Fri, 9 Oct 2020 00:13:13 +0300 Subject: [PATCH 077/680] Support stable Python3.9. (#1748) * Support stable Python3.9. * Get back to 3.9-dev * Add py39 to black usage. * remove 3.9 temporarily. --- .github/workflows/doc.yml | 4 ++-- .github/workflows/fuzz.yml | 2 +- .github/workflows/primer.yml | 2 +- .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/contributing_to_black.md | 2 +- docs/installation_and_usage.md | 2 +- readthedocs.yml | 2 +- setup.py | 1 + 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 6023a02a7f7..d266e55aa02 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -21,10 +21,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 92caa0fd5c1..9aec3c08c63 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8] # Python3.9 should be added after fixing [https://github.com/Zac-HD/hypothesmith/issues/11]. steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index 9b10db0d285..b623b938345 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-latest, windows-latest] steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd0af61e7f5..e30a8b8eb68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0687aaeee52..d446e6a0806 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ unlikely to get accepted. You can still try but prepare to be disappointed. ## Technicalities -Development on the latest version of Python is preferred. As of this writing it's 3.8. +Development on the latest version of Python is preferred. As of this writing it's 3.9. You can use any operating system. I am using macOS myself and CentOS at work. Install all development dependencies using: diff --git a/README.md b/README.md index 9c10a63fe29..2ca76dd6415 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Options: -l, --line-length INTEGER How many characters per line to allow. [default: 88] - -t, --target-version [py27|py33|py34|py35|py36|py37|py38] + -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] Python versions that should be supported by Black's output. [default: per-file auto- detection] diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md index e5307adb5d0..8e332993f97 100644 --- a/docs/contributing_to_black.md +++ b/docs/contributing_to_black.md @@ -21,7 +21,7 @@ unlikely to get accepted. You can still try but prepare to be disappointed. ## Technicalities -Development on the latest version of Python is preferred. As of this writing it's 3.8. +Development on the latest version of Python is preferred. As of this writing it's 3.9. You can use any operating system. I am using macOS myself and CentOS at work. Install all development dependencies using: diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index 7d37f59264b..7de8b657adf 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -41,7 +41,7 @@ Options: -l, --line-length INTEGER How many characters per line to allow. [default: 88] - -t, --target-version [py27|py33|py34|py35|py36|py37|py38] + -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] Python versions that should be supported by Black's output. [default: per-file auto- detection] diff --git a/readthedocs.yml b/readthedocs.yml index 15065033d0f..32bcf1fa50e 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,6 +1,6 @@ version: 2 python: - version: 3.8 + version: 3.9 install: - requirements: docs/requirements.txt - method: setuptools diff --git a/setup.py b/setup.py index 12fde2568cf..14bc1ef586a 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ def get_long_description() -> str: "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", From f311d82569b9595d85c08cc8fcf5250de525e7a0 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 12 Oct 2020 22:03:00 -0700 Subject: [PATCH 078/680] Add blackd to nicely exit if missing aiohttp deps (#1761) - If no aiohttp* deps exist nicely print a helpful message and exit - There seems to be no nice way to optionally install the entry point, so lets make the entry point nicer Test: ``` cooper-mbp1:black cooper$ /tmp/tb/bin/pip install . cooper-mbp1:black cooper$ /tmp/tb/bin/blackd aiohttp dependency is not installed: No module named 'aiohttp'. Please re-install black with the '[d]' extra install to obtain aiohttp_cors: `pip install black[d]` cooper-mbp1:black cooper$ /tmp/tb/bin/pip install .[d] ... Successfully installed aiohttp-3.6.3 aiohttp-cors-0.7.0 black cooper-mbp1:black cooper$ /tmp/tb/bin/blackd blackd version 20.8b2.dev31+gdd2f86a.d20201013 listening on localhost port 45484 ``` Fixes #1688 --- src/blackd/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index d79bfe75bc2..f77a5e8e7be 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -1,13 +1,24 @@ import asyncio +import logging +import sys from concurrent.futures import Executor, ProcessPoolExecutor from datetime import datetime from functools import partial -import logging from multiprocessing import freeze_support from typing import Set, Tuple -from aiohttp import web -import aiohttp_cors +try: + from aiohttp import web + import aiohttp_cors +except ImportError as ie: + print( + f"aiohttp dependency is not installed: {ie}. " + + "Please re-install black with the '[d]' extra install " + + "to obtain aiohttp_cors: `pip install black[d]`", + file=sys.stderr, + ) + sys.exit(-1) + import black import click From 4d6a84a8294d035747404d00b88a925fc2640e1e Mon Sep 17 00:00:00 2001 From: Hadi Alqattan Date: Mon, 19 Oct 2020 00:24:33 +0300 Subject: [PATCH 079/680] Allow black's Github action params overriding. (#1755) * Allow default params overriding. * Update: docs and action.yaml. * The second contirbution, add my name to authors.md * Correct docs `with.args` example. * Just to rerun the Travis jobs. * chmod 755 --- README.md | 1 + action.yml | 2 +- Dockerfile => action/Dockerfile | 4 +++- action/entrypoint.sh | 10 ++++++++++ docs/authors.md | 1 + docs/github_actions.md | 4 +++- 6 files changed, 19 insertions(+), 3 deletions(-) rename Dockerfile => action/Dockerfile (64%) create mode 100755 action/entrypoint.sh diff --git a/README.md b/README.md index 2ca76dd6415..7cd4d7f13b2 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,7 @@ Multiple contributions by: - [Gregory P. Smith](mailto:greg@krypto.org) - Gustavo Camargo - hauntsaninja +- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) - [Heaford](mailto:dan@heaford.com) - [Hugo Barrera](mailto::hugo@barrera.io) - Hugo van Kemenade diff --git a/action.yml b/action.yml index 2ce1c0bf2ef..60bf369a3d8 100644 --- a/action.yml +++ b/action.yml @@ -6,4 +6,4 @@ branding: icon: "check-circle" runs: using: "docker" - image: "Dockerfile" + image: "action/Dockerfile" diff --git a/Dockerfile b/action/Dockerfile similarity index 64% rename from Dockerfile rename to action/Dockerfile index a03d23a1078..eb2209940db 100644 --- a/Dockerfile +++ b/action/Dockerfile @@ -5,4 +5,6 @@ ENV PYTHONUNBUFFERED 1 RUN pip install --upgrade --no-cache-dir black -ENTRYPOINT /usr/local/bin/black --check --diff . +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/action/entrypoint.sh b/action/entrypoint.sh new file mode 100755 index 00000000000..dc86fa1996e --- /dev/null +++ b/action/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +if [ $# -eq 0 ]; then + # Default (if no args provided). + sh -c "black . --check --diff" +else + # Custom args. + sh -c "black $*" +fi diff --git a/docs/authors.md b/docs/authors.md index a5349b4b9df..ebf64eaae76 100644 --- a/docs/authors.md +++ b/docs/authors.md @@ -69,6 +69,7 @@ Multiple contributions by: - [Gregory P. Smith](mailto:greg@krypto.org) - Gustavo Camargo - hauntsaninja +- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) - [Heaford](mailto:dan@heaford.com) - [Hugo Barrera](mailto::hugo@barrera.io) - Hugo van Kemenade diff --git a/docs/github_actions.md b/docs/github_actions.md index 7ff87540242..ac80c2fab6f 100644 --- a/docs/github_actions.md +++ b/docs/github_actions.md @@ -15,5 +15,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@stable + - uses: psf/black@stable # the default is equivalent to `black . --diff --check`. + with: # (optional - override the default parameters). + args: ". --diff --check" ``` From 3e3da8eef6f4ba48da1505ad68ca0e1d788f6d6b Mon Sep 17 00:00:00 2001 From: "David W.H. Swenson" Date: Sun, 18 Oct 2020 23:27:15 +0200 Subject: [PATCH 080/680] Fix GitHub markdown links to work on RTD (#1752) * Fix internal links to work on RTD Note that these still lead to GitHub, instead of staying on RTD. * Point links to better anchors --- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/authors.md | 1 + docs/change_log.md | 6 ++++++ docs/contributing_to_black.md | 2 +- docs/installation_and_usage.md | 15 ++++++++------- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d446e6a0806..a4250f78718 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ your PR. You may need to change configuration for it to pass. For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md#black-primer). ## Hygiene diff --git a/README.md b/README.md index 7cd4d7f13b2..47472810114 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ about _Black_'s changes or will overwrite _Black_'s changes. A good example of t should be configured to neither warn about nor overwrite _Black_'s changes. Actual details on _Black_ compatible configurations for various tools can be found in -[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md). +[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#black-compatible-configurations). ### Migrating your code style without ruining git blame diff --git a/docs/authors.md b/docs/authors.md index ebf64eaae76..cdf5046c446 100644 --- a/docs/authors.md +++ b/docs/authors.md @@ -133,6 +133,7 @@ Multiple contributions by: - [Paul Ganssle](mailto:p.ganssle@gmail.com) - [Paul Meinhardt](mailto:mnhrdt@gmail.com) - [Peter Bengtsson](mailto:mail@peterbe.com) +- [Peter Grayson](mailto:pete@jpgrayson.net) - [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) - pmacosta - [Quentin Pradet](mailto:quentin@pradet.me) diff --git a/docs/change_log.md b/docs/change_log.md index e183ca545b6..1ee35a4d8f9 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -17,6 +17,12 @@ - fixed a crash when PWD=/ on POSIX (#1631) +- fixed "I/O operation on closed file" when using --diff (#1664) + +- Prevent coloured diff output being interleaved with multiple files (#1673) + +- Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) + ### 20.8b1 #### _Packaging_ diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md index 8e332993f97..f0be872a578 100644 --- a/docs/contributing_to_black.md +++ b/docs/contributing_to_black.md @@ -54,7 +54,7 @@ your PR. You may need to change configuration for it to pass. For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md#). ## Hygiene diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index 7de8b657adf..c91a1c400ef 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -119,7 +119,7 @@ about _Black_'s changes or will overwrite _Black_'s changes. A good example of t should be configured to neither warn about nor overwrite _Black_'s changes. Actual details on _Black_ compatible configurations for various tools can be found in -[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md). +[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#). ## Migrating your code style without ruining git blame @@ -167,12 +167,13 @@ know!) ## NOTE: This is a beta product -_Black_ is already [successfully used](#used-by) by many projects, small and big. It -also sports a decent test suite. However, it is still very new. 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. +_Black_ is already [successfully used](https://github.com/psf/black#used-by) by many +projects, small and big. It also sports a decent test suite. However, it is still very +new. 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. Also, as a temporary safety measure, _Black_ will check that the reformatted code still produces a valid AST that is equivalent to the original. This slows it down. If you're From 3be0aa71d9e39a8fdb6f38cc796709bf4f9c11d9 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Mon, 19 Oct 2020 13:34:42 -0400 Subject: [PATCH 081/680] Update PyCharm integrations instructions to avoid running on external changes (#1769) --- docs/editor_integration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index 73107d6a4a1..21a6865d60e 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -72,7 +72,9 @@ On Windows / Linux / BSD: - Output paths to refresh: `$FilePath$` - Working directory: `$ProjectFileDir$` - - Uncheck "Auto-save edited files to trigger the watcher" in Advanced Options + - In Advanced Options + - Uncheck "Auto-save edited files to trigger the watcher" + - Uncheck "Trigger the watcher on external changes" ## Wing IDE From 407052724fa1c97ee8bcd4e96de650def00be03e Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Mon, 19 Oct 2020 20:35:26 +0300 Subject: [PATCH 082/680] Switch to pytest and tox (#1763) * Add venv to .gitignore * Use tox to run tests * Make fuzz run in tox * Split tests files * Fix import error --- .github/workflows/fuzz.yml | 7 +- .github/workflows/test.yml | 5 +- .gitignore | 1 + test_requirements.txt | 4 + tests/test_black.py | 230 +------------------------------------ tests/test_blackd.py | 192 +++++++++++++++++++++++++++++++ tests/util.py | 43 +++++++ tox.ini | 26 +++++ 8 files changed, 275 insertions(+), 233 deletions(-) create mode 100644 test_requirements.txt create mode 100644 tests/test_blackd.py create mode 100644 tests/util.py create mode 100644 tox.ini diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 9aec3c08c63..343eed10df8 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -21,11 +21,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade coverage - python -m pip install --upgrade hypothesmith - python -m pip install -e ".[d]" + python -m pip install --upgrade tox - name: Run fuzz tests run: | - coverage run fuzz.py - coverage report + tox -e fuzz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e30a8b8eb68..8dd4e4f59fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,9 +22,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade coverage - python -m pip install -e ".[d]" + python -m pip install --upgrade tox - name: Unit tests run: | - coverage run -m unittest + tox -e py diff --git a/.gitignore b/.gitignore index 6b94cacd183..3207e72ae28 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ src/_black_version.py .dmypy.json *.swp .hypothesis/ +venv/ \ No newline at end of file diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 00000000000..999d05c0106 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,4 @@ +pytest >= 6.0.1 +pytest-mock >= 3.2.0 +pytest-cases >= 2.1.2 +coverage >= 5.2.1 \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index e17f43b88d4..7927b368b5d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -22,7 +22,6 @@ Dict, Generator, List, - Tuple, Iterator, TypeVar, ) @@ -36,18 +35,10 @@ import black from black import Feature, TargetVersion -try: - import blackd - from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop - from aiohttp import web -except ImportError: - has_blackd_deps = False -else: - has_blackd_deps = True - from pathspec import PathSpec # Import other test classes +from tests.util import THIS_DIR, read_data, DETERMINISTIC_HEADER from .test_primer import PrimerCLITests # noqa: F401 @@ -55,10 +46,6 @@ ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) fs = partial(black.format_str, mode=DEFAULT_MODE) THIS_FILE = Path(__file__) -THIS_DIR = THIS_FILE.parent -PROJECT_ROOT = THIS_DIR.parent -DETERMINISTIC_HEADER = "[Deterministic header]" -EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" PY36_VERSIONS = { TargetVersion.PY36, TargetVersion.PY37, @@ -74,29 +61,6 @@ def dump_to_stderr(*output: str) -> str: return "\n" + "\n".join(output) + "\n" -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" - _input: List[str] = [] - _output: List[str] = [] - base_dir = THIS_DIR / "data" if data else PROJECT_ROOT - with open(base_dir / name, "r", encoding="utf8") as test: - lines = test.readlines() - result = _input - for line in lines: - line = line.replace(EMPTY_LINE, "") - if line.rstrip() == "# output": - result = _output - continue - - result.append(line) - 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" - - @contextmanager def cache_dir(exists: bool = True) -> Iterator[Path]: with TemporaryDirectory() as workspace: @@ -119,17 +83,6 @@ def event_loop() -> Iterator[None]: loop.close() -@contextmanager -def skip_if_exception(e: str) -> Iterator[None]: - try: - yield - except Exception as exc: - if exc.__class__.__name__ == e: - unittest.skip(f"Encountered expected exception {exc}, skipping") - else: - raise - - class FakeContext(click.Context): """A fake click Context for when calling functions that need it.""" @@ -239,9 +192,12 @@ def test_empty_ff(self) -> None: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) - def test_self(self) -> None: + def test_run_on_test_black(self) -> None: self.checkSourceFile("tests/test_black.py") + def test_run_on_test_blackd(self) -> None: + self.checkSourceFile("tests/test_blackd.py") + def test_black(self) -> None: self.checkSourceFile("src/black/__init__.py") @@ -1969,14 +1925,6 @@ def fail(*args: Any, **kwargs: Any) -> None: ): ff(THIS_FILE) - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - 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) - def test_invalid_config_return_code(self) -> None: tmp_file = Path(black.dump_to_file()) try: @@ -2053,174 +2001,6 @@ def test_bpo_33660_workaround(self) -> None: os.chdir(str(old_cwd)) -class BlackDTestCase(AioHTTPTestCase): - async def get_application(self) -> web.Application: - return blackd.make_app() - - # TODO: remove these decorators once the below is released - # https://github.com/aio-libs/aiohttp/pull/3727 - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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') - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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"") - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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)}", - ) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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.8") - await check("py2.8") - await check("3.0") - await check("pypy3.0") - await check("jython3.4") - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @unittest_run_loop - async def test_blackd_pyi(self) -> None: - source, expected = read_data("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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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("blackd_diff.py") - expected, _ = read_data("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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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("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) - await check("34", 204) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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) - - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") - @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)) - - 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 new file mode 100644 index 00000000000..9127297c54f --- /dev/null +++ b/tests/test_blackd.py @@ -0,0 +1,192 @@ +import re +import unittest +from unittest.mock import patch + +from click.testing import CliRunner + +from tests.util import read_data, DETERMINISTIC_HEADER, skip_if_exception + +try: + import blackd + from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + from aiohttp import web +except ImportError: + has_blackd_deps = False +else: + has_blackd_deps = True + + +class BlackDTestCase(AioHTTPTestCase): + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + 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() + + # TODO: remove these decorators once the below is released + # https://github.com/aio-libs/aiohttp/pull/3727 + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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') + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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"") + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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)}", + ) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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.8") + await check("py2.8") + await check("3.0") + await check("pypy3.0") + await check("jython3.4") + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_pyi(self) -> None: + source, expected = read_data("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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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("blackd_diff.py") + expected, _ = read_data("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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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("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) + await check("34", 204) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @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)) diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 00000000000..9c3d3cbc99d --- /dev/null +++ b/tests/util.py @@ -0,0 +1,43 @@ +import unittest +from contextlib import contextmanager +from pathlib import Path +from typing import List, Tuple, Iterator + +THIS_DIR = Path(__file__).parent +PROJECT_ROOT = THIS_DIR.parent +EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" +DETERMINISTIC_HEADER = "[Deterministic header]" + + +@contextmanager +def skip_if_exception(e: str) -> Iterator[None]: + try: + yield + except Exception as exc: + if exc.__class__.__name__ == e: + unittest.skip(f"Encountered expected exception {exc}, skipping") + else: + raise + + +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" + _input: List[str] = [] + _output: List[str] = [] + base_dir = THIS_DIR / "data" if data else PROJECT_ROOT + with open(base_dir / name, "r", encoding="utf8") as test: + lines = test.readlines() + result = _input + for line in lines: + line = line.replace(EMPTY_LINE, "") + if line.rstrip() == "# output": + result = _output + continue + + result.append(line) + 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" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000000..500a2cad579 --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = py{36,37,38,39},fuzz + +[testenv] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +deps = + -r{toxinidir}/test_requirements.txt +commands = + pip install -e .[d] + coverage erase + coverage run -m pytest tests + coverage report + +[testenv:fuzz] +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. +commands = + pip install -e .[d] + coverage erase + coverage run fuzz.py + coverage report \ No newline at end of file From 989ea69bd1d0a02aa6c5794d941c122f7b4958b4 Mon Sep 17 00:00:00 2001 From: Antek S <3324881+bluefish6@users.noreply.github.com> Date: Tue, 27 Oct 2020 11:52:19 +0100 Subject: [PATCH 083/680] Update readme.md with current version (#1788) * Update readme.md with current version * Update version_control_integration.md --- README.md | 2 +- docs/version_control_integration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47472810114..d34ce146e0d 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags + rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags hooks: - id: black language_version: python3 # Should be a command that runs python3.6+ diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md index 25dac308c47..2d8bc172eba 100644 --- a/docs/version_control_integration.md +++ b/docs/version_control_integration.md @@ -9,7 +9,7 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags + rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags hooks: - id: black language_version: python3 # Should be a command that runs python3.6+ From cabeb5b5457b8fd29d608eca944be6074bd3f31f Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Tue, 27 Oct 2020 21:59:43 +0100 Subject: [PATCH 084/680] Document some culprits with pre-commit (#1783) * Document some culprits with pre-commit * make pre-commit happy * don't use monospace for black & pre-commit Co-authored-by: Hugo van Kemenade * make pre-commit happy again Co-authored-by: Hugo van Kemenade --- docs/version_control_integration.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md index 2d8bc172eba..0e09854a2e5 100644 --- a/docs/version_control_integration.md +++ b/docs/version_control_integration.md @@ -23,6 +23,11 @@ for your project. See _Black_'s own [pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an example. +When using the `--diff` flag with `pre-commit`, you must also use the `--check` flag. +When you want to run _Black_ only on specific files in pre-commit, either use +pre-commit's own `files` and `exclude` or, when using _Black_'s `--include`, set +`--force-exclude` to the negated regex of `--include`. + If you're already using Python 3.7, switch the `language_version` accordingly. Finally, `stable` is a branch that tracks the latest release on PyPI. If you'd rather run on master, this is also an option. From e6cd10e7615f4df537e2eaefcf3904a4feecad1f Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Fri, 30 Oct 2020 17:12:04 +0200 Subject: [PATCH 085/680] Extract formatting tests (#1785) * Update test versions * Use parametrize to remove tests duplications * Extract sources format tests * Fix mypy errors * Fix .travis.yml --- .travis.yml | 4 +- test_requirements.txt | 9 +- tests/test_black.py | 416 ++---------------------------------------- tests/test_format.py | 98 ++++++++++ tests/util.py | 48 ++++- 5 files changed, 160 insertions(+), 415 deletions(-) create mode 100644 tests/test_format.py diff --git a/.travis.yml b/.travis.yml index 86cf24df51b..f035343c6fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,9 @@ cache: directories: - $HOME/.cache/pre-commit env: - - TEST_CMD="coverage run -m unittest" + - TEST_CMD="tox -e py" install: - - pip install coverage coveralls pre-commit + - pip install coverage coveralls pre-commit tox - pip install -e '.[d]' script: - $TEST_CMD diff --git a/test_requirements.txt b/test_requirements.txt index 999d05c0106..3e65cdb669f 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,5 @@ -pytest >= 6.0.1 -pytest-mock >= 3.2.0 -pytest-cases >= 2.1.2 -coverage >= 5.2.1 \ No newline at end of file +pytest >= 6.1.1 +pytest-mock >= 3.3.1 +pytest-cases >= 2.3.0 +coverage >= 5.3 +parameterized >= 0.7.4 \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 7927b368b5d..b0cf6ed5caa 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -5,7 +5,6 @@ from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from dataclasses import replace -from functools import partial import inspect from io import BytesIO, TextIOWrapper import os @@ -38,13 +37,19 @@ from pathspec import PathSpec # Import other test classes -from tests.util import THIS_DIR, read_data, DETERMINISTIC_HEADER +from tests.util import ( + THIS_DIR, + read_data, + DETERMINISTIC_HEADER, + BlackBaseTestCase, + DEFAULT_MODE, + fs, + ff, + dump_to_stderr, +) from .test_primer import PrimerCLITests # noqa: F401 -DEFAULT_MODE = black.FileMode(experimental_string_processing=True) -ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) -fs = partial(black.format_str, mode=DEFAULT_MODE) THIS_FILE = Path(__file__) PY36_VERSIONS = { TargetVersion.PY36, @@ -57,10 +62,6 @@ R = TypeVar("R") -def dump_to_stderr(*output: str) -> str: - return "\n" + "\n".join(output) + "\n" - - @contextmanager def cache_dir(exists: bool = True) -> Iterator[Path]: with TemporaryDirectory() as workspace: @@ -122,29 +123,7 @@ def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None sys.stderr = hold_stderr -class BlackTestCase(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: black.DebugVisitor[Any] - black.out("Expected tree:", fg="green") - try: - exp_node = black.lib2to3_parse(expected) - bdv = black.DebugVisitor() - list(bdv.visit(exp_node)) - except Exception as ve: - black.err(str(ve)) - black.out("Actual tree:", fg="red") - try: - exp_node = black.lib2to3_parse(actual) - bdv = black.DebugVisitor() - list(bdv.visit(exp_node)) - except Exception as ve: - black.err(str(ve)) - self.assertMultiLineEqual(expected, actual) - +class BlackTestCase(BlackBaseTestCase): def invokeBlack( self, args: List[str], exit_code: int = 0, ignore_config: bool = True ) -> None: @@ -163,16 +142,6 @@ def invokeBlack( ), ) - @patch("black.dump_to_file", dump_to_stderr) - def checkSourceFile(self, name: str, mode: black.FileMode = DEFAULT_MODE) -> None: - path = THIS_DIR.parent / name - source, expected = read_data(str(path), data=False) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) - self.assertFalse(ff(path)) - @patch("black.dump_to_file", dump_to_stderr) def test_empty(self) -> None: source = expected = "" @@ -192,48 +161,6 @@ def test_empty_ff(self) -> None: os.unlink(tmp_file) self.assertFormatEqual(expected, actual) - def test_run_on_test_black(self) -> None: - self.checkSourceFile("tests/test_black.py") - - def test_run_on_test_blackd(self) -> None: - self.checkSourceFile("tests/test_blackd.py") - - def test_black(self) -> None: - self.checkSourceFile("src/black/__init__.py") - - def test_pygram(self) -> None: - self.checkSourceFile("src/blib2to3/pygram.py") - - def test_pytree(self) -> None: - self.checkSourceFile("src/blib2to3/pytree.py") - - def test_conv(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/conv.py") - - def test_driver(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/driver.py") - - def test_grammar(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/grammar.py") - - def test_literals(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/literals.py") - - def test_parse(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/parse.py") - - def test_pgen(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/pgen.py") - - def test_tokenize(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/tokenize.py") - - def test_token(self) -> None: - self.checkSourceFile("src/blib2to3/pgen2/token.py") - - def test_setup(self) -> None: - self.checkSourceFile("setup.py") - def test_piping(self) -> None: source, expected = read_data("src/black/__init__", data=False) result = BlackRunner().invoke( @@ -291,22 +218,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_function(self) -> None: - source, expected = read_data("function") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_function2(self) -> None: - source, expected = read_data("function2") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) def _test_wip(self) -> None: source, expected = read_data("wip") @@ -322,14 +233,6 @@ def _test_wip(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, black.FileMode()) - @patch("black.dump_to_file", dump_to_stderr) - def test_function_trailing_comma(self) -> None: - source, expected = read_data("function_trailing_comma") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @unittest.expectedFailure @patch("black.dump_to_file", dump_to_stderr) def test_trailing_comma_optional_parens_stability1(self) -> None: @@ -351,14 +254,6 @@ def test_trailing_comma_optional_parens_stability3(self) -> None: actual = fs(source) black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) - def test_expression(self) -> None: - source, expected = read_data("expression") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - 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") @@ -434,14 +329,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_fstring(self) -> None: - source, expected = read_data("fstring") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) def test_pep_570(self) -> None: source, expected = read_data("pep_570") @@ -472,14 +359,6 @@ def test_string_quotes(self) -> None: 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(self) -> None: - source, expected = read_data("docstring") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_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.""" @@ -490,14 +369,6 @@ def test_docstring_no_string_normalization(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, mode) - def test_long_strings(self) -> None: - """Tests for splitting long strings.""" - source, expected = read_data("long_strings") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_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") @@ -506,162 +377,6 @@ def test_long_strings_flag_disabled(self) -> None: self.assertFormatEqual(expected, actual) black.assert_stable(expected, actual, mode) - @patch("black.dump_to_file", dump_to_stderr) - def test_long_strings__edge_case(self) -> None: - """Edge-case tests for splitting long strings.""" - source, expected = read_data("long_strings__edge_case") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_long_strings__regression(self) -> None: - """Regression tests for splitting long strings.""" - source, expected = read_data("long_strings__regression") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_slices(self) -> None: - source, expected = read_data("slices") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_percent_precedence(self) -> None: - source, expected = read_data("percent_precedence") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments(self) -> None: - source, expected = read_data("comments") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments2(self) -> None: - source, expected = read_data("comments2") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments3(self) -> None: - source, expected = read_data("comments3") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments4(self) -> None: - source, expected = read_data("comments4") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments5(self) -> None: - source, expected = read_data("comments5") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments6(self) -> None: - source, expected = read_data("comments6") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comments7(self) -> None: - source, expected = read_data("comments7") - mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_comment_after_escaped_newline(self) -> None: - source, expected = read_data("comment_after_escaped_newline") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_cantfit(self) -> None: - source, expected = read_data("cantfit") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_import_spacing(self) -> None: - source, expected = read_data("import_spacing") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_composition(self) -> None: - source, expected = read_data("composition") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_composition_no_trailing_comma(self) -> None: - source, expected = read_data("composition_no_trailing_comma") - mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) - actual = fs(source, mode=mode) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_empty_lines(self) -> None: - source, expected = read_data("empty_lines") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_remove_parens(self) -> None: - source, expected = read_data("remove_parens") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_string_prefixes(self) -> None: - source, expected = read_data("string_prefixes") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) def test_numeric_literals(self) -> None: source, expected = read_data("numeric_literals") @@ -680,21 +395,6 @@ def test_numeric_literals_ignoring_underscores(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, mode) - @patch("black.dump_to_file", dump_to_stderr) - def test_numeric_literals_py2(self) -> None: - source, expected = read_data("numeric_literals_py2") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_python2(self) -> None: - source, expected = read_data("python2") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: source, expected = read_data("python2_print_function") @@ -704,14 +404,6 @@ def test_python2_print_function(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, mode) - @patch("black.dump_to_file", dump_to_stderr) - def test_python2_unicode_literals(self) -> None: - source, expected = read_data("python2_unicode_literals") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) def test_stub(self) -> None: mode = replace(DEFAULT_MODE, is_pyi=True) @@ -770,78 +462,6 @@ def test_python39(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, DEFAULT_MODE) - @patch("black.dump_to_file", dump_to_stderr) - def test_fmtonoff(self) -> None: - source, expected = read_data("fmtonoff") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_fmtonoff2(self) -> None: - source, expected = read_data("fmtonoff2") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_fmtonoff3(self) -> None: - source, expected = read_data("fmtonoff3") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_fmtonoff4(self) -> None: - source, expected = read_data("fmtonoff4") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_remove_empty_parentheses_after_class(self) -> None: - source, expected = read_data("class_blank_parentheses") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_new_line_between_class_and_code(self) -> None: - source, expected = read_data("class_methods_new_line") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_bracket_match(self) -> None: - source, expected = read_data("bracketmatch") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_tuple_assign(self) -> None: - source, expected = read_data("tupleassign") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - - @patch("black.dump_to_file", dump_to_stderr) - def test_beginning_backslash(self) -> None: - source, expected = read_data("beginning_backslash") - actual = fs(source) - self.assertFormatEqual(expected, actual) - 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" @@ -1569,13 +1189,6 @@ def test_read_cache_line_lengths(self) -> None: two = black.read_cache(short_mode) self.assertNotIn(path, two) - def test_tricky_unicode_symbols(self) -> None: - source, expected = read_data("tricky_unicode_symbols") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - def test_single_file_force_pyi(self) -> None: pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") @@ -1672,13 +1285,6 @@ def test_multi_file_force_py36(self) -> None: self.assertIn(path, pyi_cache) self.assertNotIn(path, normal_cache) - def test_collections(self) -> None: - source, expected = read_data("collections") - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") result = CliRunner().invoke( diff --git a/tests/test_format.py b/tests/test_format.py new file mode 100644 index 00000000000..e4677707e3c --- /dev/null +++ b/tests/test_format.py @@ -0,0 +1,98 @@ +from unittest.mock import patch + +import black +from parameterized import parameterized + +from tests.util import ( + BlackBaseTestCase, + fs, + ff, + DEFAULT_MODE, + dump_to_stderr, + read_data, + THIS_DIR, +) + +SIMPLE_CASES = [ + "beginning_backslash", + "bracketmatch", + "cantfit", + "class_blank_parentheses", + "class_methods_new_line", + "collections", + "comments", + "comments2", + "comments3", + "comments4", + "comments5", + "comments6", + "comments7", + "comment_after_escaped_newline", + "composition", + "composition_no_trailing_comma", + "docstring", + "empty_lines", + "expression", + "fmtonoff", + "fmtonoff2", + "fmtonoff3", + "fmtonoff4", + "fstring", + "function", + "function2", + "function_trailing_comma", + "import_spacing", + "long_strings", + "long_strings__edge_case", + "long_strings__regression", + "numeric_literals_py2", + "percent_precedence", + "python2", + "python2_unicode_literals", + "remove_parens", + "slices", + "string_prefixes", + "tricky_unicode_symbols", + "tupleassign", +] + + +SOURCES = [ + "tests/test_black.py", + "tests/test_format.py", + "tests/test_blackd.py", + "src/black/__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", +] + + +class TestSimpleFormat(BlackBaseTestCase): + @parameterized.expand(SIMPLE_CASES) + @patch("black.dump_to_file", dump_to_stderr) + def test_simple_format(self, filename: str) -> None: + source, expected = read_data(filename) + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) + + @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 + source, expected = read_data(str(path), data=False) + actual = fs(source, mode=DEFAULT_MODE) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) + self.assertFalse(ff(path)) diff --git a/tests/util.py b/tests/util.py index 9c3d3cbc99d..da65ed0cc93 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,7 +1,10 @@ +import os import unittest from contextlib import contextmanager from pathlib import Path -from typing import List, Tuple, Iterator +from typing import List, Tuple, Iterator, Any +import black +from functools import partial THIS_DIR = Path(__file__).parent PROJECT_ROOT = THIS_DIR.parent @@ -9,6 +12,39 @@ DETERMINISTIC_HEADER = "[Deterministic header]" +DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) +fs = partial(black.format_str, mode=DEFAULT_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: black.DebugVisitor[Any] + black.out("Expected tree:", fg="green") + try: + exp_node = black.lib2to3_parse(expected) + bdv = black.DebugVisitor() + list(bdv.visit(exp_node)) + except Exception as ve: + black.err(str(ve)) + black.out("Actual tree:", fg="red") + try: + exp_node = black.lib2to3_parse(actual) + bdv = black.DebugVisitor() + list(bdv.visit(exp_node)) + except Exception as ve: + black.err(str(ve)) + self.assertMultiLineEqual(expected, actual) + + @contextmanager def skip_if_exception(e: str) -> Iterator[None]: try: @@ -24,11 +60,15 @@ 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" - _input: List[str] = [] - _output: List[str] = [] base_dir = THIS_DIR / "data" if data else PROJECT_ROOT - with open(base_dir / name, "r", encoding="utf8") as test: + return read_data_from_file(base_dir / name) + + +def read_data_from_file(file_name: Path) -> Tuple[str, str]: + with open(file_name, "r", encoding="utf8") as test: lines = test.readlines() + _input: List[str] = [] + _output: List[str] = [] result = _input for line in lines: line = line.replace(EMPTY_LINE, "") From b64fd2bbec420ff71f7592f40816ae7de2fa4a15 Mon Sep 17 00:00:00 2001 From: Shota Ray Imaki Date: Sat, 31 Oct 2020 00:13:55 +0900 Subject: [PATCH 086/680] Add compatible configuration files. (psf#1789) (#1792) * Add compatible configuration files. (psf#1789) * Simplify isort configuration files. (#1789) --- docs/compatible_configs.md | 3 +++ docs/compatible_configs/flake8/.flake8 | 3 +++ docs/compatible_configs/flake8/setup.cfg | 3 +++ docs/compatible_configs/flake8/tox.ini | 3 +++ docs/compatible_configs/isort/.editorconfig | 2 ++ docs/compatible_configs/isort/.isort.cfg | 2 ++ docs/compatible_configs/isort/pyproject.toml | 2 ++ docs/compatible_configs/isort/setup.cfg | 2 ++ docs/compatible_configs/pylint/pylintrc | 5 +++++ docs/compatible_configs/pylint/pyproject.toml | 5 +++++ docs/compatible_configs/pylint/setup.cfg | 5 +++++ 11 files changed, 35 insertions(+) create mode 100644 docs/compatible_configs/flake8/.flake8 create mode 100644 docs/compatible_configs/flake8/setup.cfg create mode 100644 docs/compatible_configs/flake8/tox.ini create mode 100644 docs/compatible_configs/isort/.editorconfig create mode 100644 docs/compatible_configs/isort/.isort.cfg create mode 100644 docs/compatible_configs/isort/pyproject.toml create mode 100644 docs/compatible_configs/isort/setup.cfg create mode 100644 docs/compatible_configs/pylint/pylintrc create mode 100644 docs/compatible_configs/pylint/pyproject.toml create mode 100644 docs/compatible_configs/pylint/setup.cfg diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index 990820a6771..c6724e77e57 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -10,6 +10,9 @@ tools out there. (e.g. `pyproject.toml`). The provided examples are to only configure their corresponding tools, using **their** supported file formats. +Compatible configuration files can be +[found here](https://github.com/psf/black/blob/master/docs/compatible_configs/). + ## isort [isort](https://pypi.org/p/isort/) helps to sort and format imports in your Python code. diff --git a/docs/compatible_configs/flake8/.flake8 b/docs/compatible_configs/flake8/.flake8 new file mode 100644 index 00000000000..8dd399ab55b --- /dev/null +++ b/docs/compatible_configs/flake8/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/docs/compatible_configs/flake8/setup.cfg b/docs/compatible_configs/flake8/setup.cfg new file mode 100644 index 00000000000..8dd399ab55b --- /dev/null +++ b/docs/compatible_configs/flake8/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/docs/compatible_configs/flake8/tox.ini b/docs/compatible_configs/flake8/tox.ini new file mode 100644 index 00000000000..8dd399ab55b --- /dev/null +++ b/docs/compatible_configs/flake8/tox.ini @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/docs/compatible_configs/isort/.editorconfig b/docs/compatible_configs/isort/.editorconfig new file mode 100644 index 00000000000..edc849a54a6 --- /dev/null +++ b/docs/compatible_configs/isort/.editorconfig @@ -0,0 +1,2 @@ +[*.py] +profile = black diff --git a/docs/compatible_configs/isort/.isort.cfg b/docs/compatible_configs/isort/.isort.cfg new file mode 100644 index 00000000000..f238bf7ea13 --- /dev/null +++ b/docs/compatible_configs/isort/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile = black diff --git a/docs/compatible_configs/isort/pyproject.toml b/docs/compatible_configs/isort/pyproject.toml new file mode 100644 index 00000000000..cea9915bebf --- /dev/null +++ b/docs/compatible_configs/isort/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +profile = black diff --git a/docs/compatible_configs/isort/setup.cfg b/docs/compatible_configs/isort/setup.cfg new file mode 100644 index 00000000000..c76db01ff4e --- /dev/null +++ b/docs/compatible_configs/isort/setup.cfg @@ -0,0 +1,2 @@ +[isort] +profile = black diff --git a/docs/compatible_configs/pylint/pylintrc b/docs/compatible_configs/pylint/pylintrc new file mode 100644 index 00000000000..7abddd2c330 --- /dev/null +++ b/docs/compatible_configs/pylint/pylintrc @@ -0,0 +1,5 @@ +[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 new file mode 100644 index 00000000000..49ad7a2c771 --- /dev/null +++ b/docs/compatible_configs/pylint/pyproject.toml @@ -0,0 +1,5 @@ +[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 new file mode 100644 index 00000000000..3ada24530ea --- /dev/null +++ b/docs/compatible_configs/pylint/setup.cfg @@ -0,0 +1,5 @@ +[pylint] +max-line-length = 88 + +[pylint.messages_control] +disable = C0330, C0326 From 5d978df8b54d4fb102142508353a04932a4b5b04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2020 08:15:09 -0700 Subject: [PATCH 087/680] Bump cryptography from 3.1 to 3.2 (#1791) Bumps [cryptography](https://github.com/pyca/cryptography) from 3.1 to 3.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.1...3.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 355 +++++++++++++++++++++++++-------------------------- 1 file changed, 172 insertions(+), 183 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index e09d69ea83d..95b7613114a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -53,7 +53,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -61,7 +60,6 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "black": { @@ -92,16 +90,13 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "python_version <": "3.7", - "version": "==0.6", - "version >": "0.6" + "version": "==0.6" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "multidict": { @@ -124,7 +119,6 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "mypy-extensions": { @@ -170,26 +164,10 @@ "index": "pypi", "version": "==2020.7.14" }, - "setuptools-scm": { - "hashes": [ - "sha256:09c659d1d6680811c43f476a33c6d3d9872416580786e96bd29ea03e6a818e41", - "sha256:69258e2eeba5f7ce1ed7a5f109519580fa3578250f8e4d6684859f86d1b15826", - "sha256:731550a27e3901ee501c3bf99140b5436b8eeba341a9d19911cf364b8d573293", - "sha256:892e63b4983f9e30f9e8bf89ad03d2a02a47e6e5ced09b03ae6fe952ade8a579", - "sha256:a8994582e716ec690f33fec70cca0f85bd23ec974e3f783233e4879090a7faa8", - "sha256:b42c150c34d6120babf3646abd7513e032be2e230b3d2034f27404c65aa0c977", - "sha256:eaaec16b7af25c5f532b5af2332213bb6223d15cca4518f6dbc4c055641c86fd", - "sha256:efc928d6a64162c88cdc85fa4b84adfbd6dbf9f9b04319bc495eb16dcfaae00a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.1.2" - }, "toml": { "hashes": [ - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" @@ -198,9 +176,11 @@ "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", @@ -209,12 +189,16 @@ "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], @@ -232,26 +216,41 @@ }, "yarl": { "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "sha256:03b7a44384ad60be1b7be93c2a24dc74895f8d767ea0bce15b2f6fc7695a3843", + "sha256:076157404db9db4bb3fa9db22db319bbb36d075eeab19ba018ce20ae0cacf037", + "sha256:1c05ae3d5ea4287470046a2c2754f0a4c171b84ea72c8a691f776eb1753dfb91", + "sha256:2467baf8233f7c64048df37e11879c553943ffe7f373e689711ec2807ea13805", + "sha256:2bb2e21cf062dfbe985c3cd4618bae9f25271efcad9e7be1277861247eee9839", + "sha256:311effab3b3828ab34f0e661bb57ff422f67d5c33056298bda4c12195251f8dd", + "sha256:3526cb5905907f0e42bee7ef57ae4a5f02bc27dcac27859269e2bba0caa4c2b6", + "sha256:39b1e586f34b1d2512c9b39aa3cf24c870c972d525e36edc9ee19065db4737bb", + "sha256:4bed5cd7c8e69551eb19df15295ba90e62b9a6a1149c76eb4a9bab194402a156", + "sha256:51c6d3cf7a1f1fbe134bb92f33b7affd94d6de24cd64b466eb12de52120fb8c6", + "sha256:59f78b5da34ddcffb663b772f7619e296518712e022e57fc5d9f921818e2ab7c", + "sha256:6f29115b0c330da25a04f48612d75333bca04521181a666ca0b8761005a99150", + "sha256:73d4e1e1ef5e52d526c92f07d16329e1678612c6a81dd8101fdcae11a72de15c", + "sha256:9b48d31f8d881713fd461abfe7acbb4dcfeb47cec3056aa83f2fbcd2244577f7", + "sha256:a1fd575dd058e10ad4c35065e7c3007cc74d142f622b14e168d8a273a2fa8713", + "sha256:b3dd1052afd436ba737e61f5d3bed1f43a7f9a33fc58fbe4226eb919a7006019", + "sha256:b99c25ed5c355b35d1e6dae87ac7297a4844a57dc5766b173b88b6163a36eb0d", + "sha256:c056e86bff5a0b566e0d9fab4f67e83b12ae9cbcd250d334cbe2005bbe8c96f2", + "sha256:c45b49b59a5724869899798e1bbd447ac486215269511d3b76b4c235a1b766b6", + "sha256:cd623170c729a865037828e3f99f8ebdb22a467177a539680dfc5670b74c84e2", + "sha256:d25d3311794e6c71b608d7c47651c8f65eea5ab15358a27f29330b3475e8f8e5", + "sha256:d695439c201ed340745250f9eb4dfe8d32bf1e680c16477107b8f3ce4bff4fdb", + "sha256:d77f6c9133d2aabb290a7846aaa74ec14d7b5ab35b01591fac5a70c4a8c959a2", + "sha256:d894a2442d2cd20a3b0b0dce5a353d316c57d25a2b445e03f7eac90eee27b8af", + "sha256:db643ce2b58a4bd11a82348225c53c76ecdd82bb37cf4c085e6df1b676f4038c", + "sha256:e3a0c43a26dfed955b2a06fdc4d51d2c51bc2200aff8ce8faf14e676ea8c8862", + "sha256:e77bf79ad1ccae672eab22453838382fe9029fc27c8029e84913855512a587d8", + "sha256:f2f0174cb15435957d3b751093f89aede77df59a499ab7516bbb633b77ead13a", + "sha256:f3031c78edf10315abe232254e6a36b65afe65fded41ee54ed7976d0b2cdf0da", + "sha256:f4c007156732866aa4507d619fe6f8f2748caabed4f66b276ccd97c82572620c", + "sha256:f4f27ff3dd80bc7c402def211a47291ea123d59a23f59fe18fc0e81e3e71f385", + "sha256:f57744fc61e118b5d114ae8077d8eb9df4d2d2c11e2af194e21f0c11ed9dcf6c", + "sha256:f835015a825980b65356e9520979a1564c56efea7da7d4b68a14d4a07a3a7336" + ], + "version": "==1.6.2" } }, "develop": { @@ -301,7 +300,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -309,7 +307,6 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "babel": { @@ -317,7 +314,6 @@ "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.0" }, "black": { @@ -332,7 +328,6 @@ "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.2.1" }, "certifi": { @@ -344,43 +339,50 @@ }, "cffi": { "hashes": [ - "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", - "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", - "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", - "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", - "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", - "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", - "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", - "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", - "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", - "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", - "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", - "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", - "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", - "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", - "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", - "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", - "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", - "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", - "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", - "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", - "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", - "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", - "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", - "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", - "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", - "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", - "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", - "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" - ], - "version": "==1.14.2" + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -400,11 +402,10 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "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.3" + "version": "==0.4.4" }, "commonmark": { "hashes": [ @@ -455,31 +456,31 @@ }, "cryptography": { "hashes": [ - "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a", - "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed", - "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36", - "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08", - "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237", - "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618", - "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f", - "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695", - "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c", - "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10", - "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c", - "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1", - "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e", - "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f", - "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791", - "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0", - "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af", - "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8", - "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761", - "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716", - "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32", - "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1" + "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", + "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", + "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", + "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", + "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", + "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", + "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", + "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", + "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", + "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", + "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", + "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", + "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", + "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", + "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", + "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", + "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", + "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", + "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", + "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", + "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", + "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" + ], + "index": "pypi", + "version": "==3.2" }, "distlib": { "hashes": [ @@ -491,7 +492,6 @@ "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", - "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -530,18 +530,16 @@ }, "identify": { "hashes": [ - "sha256:c770074ae1f19e08aadbda1c886bc6d0cb55ffdc503a8c0fe8699af2fc9664ae", - "sha256:d02d004568c5a01261839a05e91705e3e9f5c57a3551648f9b3fb2b9c62c0f62" + "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", + "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.3" + "version": "==1.5.6" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -549,7 +547,6 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "jeepney": { @@ -565,7 +562,6 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "keyring": { @@ -573,7 +569,6 @@ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], - "markers": "python_version >= '3.6'", "version": "==21.4.0" }, "markupsafe": { @@ -612,7 +607,6 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -642,7 +636,6 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "mypy": { @@ -685,7 +678,6 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pathspec": { @@ -698,10 +690,10 @@ }, "pkginfo": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", + "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" ], - "version": "==1.5.0.1" + "version": "==1.6.1" }, "pre-commit": { "hashes": [ @@ -716,7 +708,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pycparser": { @@ -724,7 +715,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -732,23 +722,20 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { "hashes": [ - "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", - "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" + "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", + "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.1" + "version": "==2.7.2" }, "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" }, "pytz": { @@ -822,7 +809,6 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requests-toolbelt": { @@ -849,16 +835,10 @@ }, "setuptools-scm": { "hashes": [ - "sha256:09c659d1d6680811c43f476a33c6d3d9872416580786e96bd29ea03e6a818e41", "sha256:69258e2eeba5f7ce1ed7a5f109519580fa3578250f8e4d6684859f86d1b15826", - "sha256:731550a27e3901ee501c3bf99140b5436b8eeba341a9d19911cf364b8d573293", - "sha256:892e63b4983f9e30f9e8bf89ad03d2a02a47e6e5ced09b03ae6fe952ade8a579", - "sha256:a8994582e716ec690f33fec70cca0f85bd23ec974e3f783233e4879090a7faa8", - "sha256:b42c150c34d6120babf3646abd7513e032be2e230b3d2034f27404c65aa0c977", - "sha256:eaaec16b7af25c5f532b5af2332213bb6223d15cca4518f6dbc4c055641c86fd", - "sha256:efc928d6a64162c88cdc85fa4b84adfbd6dbf9f9b04319bc495eb16dcfaae00a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "sha256:a8994582e716ec690f33fec70cca0f85bd23ec974e3f783233e4879090a7faa8" + ], + "index": "pypi", "version": "==4.1.2" }, "six": { @@ -866,7 +846,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -889,7 +868,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -897,7 +875,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -905,7 +882,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -913,7 +889,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -921,7 +896,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -929,26 +903,22 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { "hashes": [ - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c" + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" }, "tqdm": { "hashes": [ - "sha256:8f3c5815e3b5e20bc40463fa6b42a352178859692a68ffaa469706e6d38342a5", - "sha256:faf9c671bd3fad5ebaeee366949d969dca2b2be32c872a7092a1e1a9048d105b" + "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", + "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.49.0" + "version": "==4.51.0" }, "twine": { "hashes": [ @@ -962,9 +932,11 @@ "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", @@ -973,12 +945,16 @@ "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], @@ -996,19 +972,17 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "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.25.10" + "version": "==1.25.11" }, "virtualenv": { "hashes": [ - "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", - "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.31" + "version": "==20.1.0" }, "webencodings": { "hashes": [ @@ -1027,26 +1001,41 @@ }, "yarl": { "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "sha256:03b7a44384ad60be1b7be93c2a24dc74895f8d767ea0bce15b2f6fc7695a3843", + "sha256:076157404db9db4bb3fa9db22db319bbb36d075eeab19ba018ce20ae0cacf037", + "sha256:1c05ae3d5ea4287470046a2c2754f0a4c171b84ea72c8a691f776eb1753dfb91", + "sha256:2467baf8233f7c64048df37e11879c553943ffe7f373e689711ec2807ea13805", + "sha256:2bb2e21cf062dfbe985c3cd4618bae9f25271efcad9e7be1277861247eee9839", + "sha256:311effab3b3828ab34f0e661bb57ff422f67d5c33056298bda4c12195251f8dd", + "sha256:3526cb5905907f0e42bee7ef57ae4a5f02bc27dcac27859269e2bba0caa4c2b6", + "sha256:39b1e586f34b1d2512c9b39aa3cf24c870c972d525e36edc9ee19065db4737bb", + "sha256:4bed5cd7c8e69551eb19df15295ba90e62b9a6a1149c76eb4a9bab194402a156", + "sha256:51c6d3cf7a1f1fbe134bb92f33b7affd94d6de24cd64b466eb12de52120fb8c6", + "sha256:59f78b5da34ddcffb663b772f7619e296518712e022e57fc5d9f921818e2ab7c", + "sha256:6f29115b0c330da25a04f48612d75333bca04521181a666ca0b8761005a99150", + "sha256:73d4e1e1ef5e52d526c92f07d16329e1678612c6a81dd8101fdcae11a72de15c", + "sha256:9b48d31f8d881713fd461abfe7acbb4dcfeb47cec3056aa83f2fbcd2244577f7", + "sha256:a1fd575dd058e10ad4c35065e7c3007cc74d142f622b14e168d8a273a2fa8713", + "sha256:b3dd1052afd436ba737e61f5d3bed1f43a7f9a33fc58fbe4226eb919a7006019", + "sha256:b99c25ed5c355b35d1e6dae87ac7297a4844a57dc5766b173b88b6163a36eb0d", + "sha256:c056e86bff5a0b566e0d9fab4f67e83b12ae9cbcd250d334cbe2005bbe8c96f2", + "sha256:c45b49b59a5724869899798e1bbd447ac486215269511d3b76b4c235a1b766b6", + "sha256:cd623170c729a865037828e3f99f8ebdb22a467177a539680dfc5670b74c84e2", + "sha256:d25d3311794e6c71b608d7c47651c8f65eea5ab15358a27f29330b3475e8f8e5", + "sha256:d695439c201ed340745250f9eb4dfe8d32bf1e680c16477107b8f3ce4bff4fdb", + "sha256:d77f6c9133d2aabb290a7846aaa74ec14d7b5ab35b01591fac5a70c4a8c959a2", + "sha256:d894a2442d2cd20a3b0b0dce5a353d316c57d25a2b445e03f7eac90eee27b8af", + "sha256:db643ce2b58a4bd11a82348225c53c76ecdd82bb37cf4c085e6df1b676f4038c", + "sha256:e3a0c43a26dfed955b2a06fdc4d51d2c51bc2200aff8ce8faf14e676ea8c8862", + "sha256:e77bf79ad1ccae672eab22453838382fe9029fc27c8029e84913855512a587d8", + "sha256:f2f0174cb15435957d3b751093f89aede77df59a499ab7516bbb633b77ead13a", + "sha256:f3031c78edf10315abe232254e6a36b65afe65fded41ee54ed7976d0b2cdf0da", + "sha256:f4c007156732866aa4507d619fe6f8f2748caabed4f66b276ccd97c82572620c", + "sha256:f4f27ff3dd80bc7c402def211a47291ea123d59a23f59fe18fc0e81e3e71f385", + "sha256:f57744fc61e118b5d114ae8077d8eb9df4d2d2c11e2af194e21f0c11ed9dcf6c", + "sha256:f835015a825980b65356e9520979a1564c56efea7da7d4b68a14d4a07a3a7336" + ], + "version": "==1.6.2" } } } From 8c8af4f1612dc29358490a7d04dd4ff2f5d4939d Mon Sep 17 00:00:00 2001 From: Abdullah Selek Date: Fri, 30 Oct 2020 15:24:18 +0000 Subject: [PATCH 088/680] Start using Python 3.9 on Travis (#1790) * Start using Python 3.9 on Travis * Remove allow_failures --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f035343c6fb..e8d2975c5ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,9 +28,7 @@ matrix: - name: "3.8" python: 3.8 - name: "3.9" - python: 3.9-dev - allow_failures: - - python: 3.9-dev + python: 3.9 before_deploy: - pip install pyinstaller - pyinstaller --clean -F --add-data src/blib2to3/:blib2to3 src/black/__init__.py From 6c3f8181854160f3db354f6f6ef3315ef05db8e7 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sat, 31 Oct 2020 13:42:36 -0400 Subject: [PATCH 089/680] Fix bug where black tries to split string on escaped space (#1799) Closes #1505. --- src/black/__init__.py | 17 ++++++++++-- tests/data/long_strings__regression.py | 37 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 24e9d4edaaa..e09838d866a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3590,7 +3590,8 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): MIN_SUBSTR_SIZE characters. The string will ONLY be split on spaces (i.e. each new substring should - start with a space). + start with a space). Note that the string will NOT be split on a space + which is escaped with a backslash. If the string is an f-string, it will NOT be split in the middle of an f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x @@ -3930,11 +3931,23 @@ def passes_all_checks(i: Index) -> bool: section of this classes' docstring would be be met by returning @i. """ is_space = string[i] == " " + + is_not_escaped = True + j = i - 1 + while is_valid_index(j) and string[j] == "\\": + is_not_escaped = not is_not_escaped + j -= 1 + is_big_enough = ( len(string[i:]) >= self.MIN_SUBSTR_SIZE and len(string[:i]) >= self.MIN_SUBSTR_SIZE ) - return is_space and is_big_enough and not breaks_fstring_expression(i) + return ( + is_space + and is_not_escaped + and is_big_enough + and not breaks_fstring_expression(i) + ) # First, we check all indices BELOW @max_break_idx. break_idx = max_break_idx diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 8290a4cbc1c..9bfb1eedd5a 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -353,6 +353,24 @@ def xxxxxxx_xxxxxx(xxxx): 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( + r"(? Date: Sun, 1 Nov 2020 16:17:23 -0500 Subject: [PATCH 090/680] Automatically build and upload binaries on release (#1743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new GitHub Actions workflow that builds self-contained binaries / executables and uploads them as release assets to the triggering release. Publishing a release, drafting one doesn't count, will trigger this workflow. I personally used GitHub Actions only because it's the CI/CD platform(?) I am familiar with. Only Windows and Linux binaries are supported since I don't have any systems running Mac OS. For Linux, I had originally planned to use the manylinux2010 docker image the PyPA provides for highly compatible wheel building, but unfortunately it wasn't feasible due to GitHub Actions and PyInstaller incompatibilities. As a stopgap the oldest versions of Linux and Windows are used although Windows Server 2019 isn't that old nor is Ubuntu 16.04! I guess someone (maybe me) could work out something else if compatibility is big problem. A few things you should know about the workflow: - You don't need to set the `GITHUB_TOKEN` secret as it is automatically provided by GitHub. - matrix.pathsep is used because PyInstaller configuration's format is OS dependent for some reason ... Also it's worth mentioning that Black once had Travis CI and AppVeyor configuration that did the same thing as this commit. They were committed in mid 2018 and worked (somewhat) well. Eventually we stopped using AppVeyor and the refactor to packages broke the Travis CI config. This commit replaces the still existing and broken Travis CI config wholesale. Co-authored-by: Anders Fredrik Kiær <31612826+anders-kiaer@users.noreply.github.com> - Anders told me that I could get the release asset upload URL directly from the github.event.release payload. I originally planned to use bruceadams/get-release to get such URL. --- .github/workflows/upload_binary.yml | 51 +++++++++++++++++++++++++++++ .travis.yml | 14 -------- CHANGES.md | 5 +++ 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/upload_binary.yml diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml new file mode 100644 index 00000000000..46d92ab2149 --- /dev/null +++ b/.github/workflows/upload_binary.yml @@ -0,0 +1,51 @@ +name: Upload self-contained binaries + +on: + release: + types: [published] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [3.7] + os: [ubuntu-16.04, windows-2019] + include: + - os: windows-2019 + pathsep: ";" + executable_suffix: ".exe" + executable_mime: "application/vnd.microsoft.portable-executable" + - os: ubuntu-16.04 + pathsep: ":" + executable_suffix: ".elf" + executable_mime: "application/x-executable" + + 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 wheel setuptools + python -m pip install . + python -m pip install pyinstaller + + - name: Build binary + run: | + python -m PyInstaller -F --name black${{ matrix.executable_suffix }} --add-data 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py + + - name: Upload binary as release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: dist/black${{ matrix.executable_suffix }} + asset_name: black${{ matrix.executable_suffix }} + asset_content_type: ${{ matrix.executable_mime }} diff --git a/.travis.yml b/.travis.yml index e8d2975c5ff..e782272e813 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,17 +29,3 @@ matrix: python: 3.8 - name: "3.9" python: 3.9 -before_deploy: - - pip install pyinstaller - - pyinstaller --clean -F --add-data src/blib2to3/:blib2to3 src/black/__init__.py -deploy: - provider: releases - api_key: - secure: chYvcmnRqRKtfBcAZRj62rEv0ziWuHMl6MnfQbd1MOVQ4njntI8+CCPk118dW6MWSfwTqyMFy+t9gAgQYhjkLEHMS2aK9Z2wCWki1MkBrkMw5tYoLFvPu0KQ9rIVihxsr93a/am6Oh/Hp+1uuc4zWPUf1ubX+QlCzsxjCzVso1kTJjjdN04UxvkcFR+sY2d9Qyy9WcdifChnLwdmIJKIoVOE7Imm820nzImJHkJh8iSnjBjL98gvPPeC/nWTltsbErvf2mCv4NIjzjQZvHa87c7rSJGbliNrAxCSyyvBX+JNeS8U2fGLE83do0HieyjdPbTuc27e2nsrrihgPh+hXbiJerljclfp5hsJ5qGz5sS9MU1fR7sSLiQQ2v0TYB5RRwd34TgGiLwFAZZmgZOfMUCtefCKvP8qvELMSNd99+msfPEHiuhADF0bKPTbCUa6BgUHNr6woOLmHerjPHd6NI/a8Skz/uQB4xr3spLSmfUmX0fEqyYUDphkGPNH8IsvC1/F2isecW9kOzEWmB5oCmpMTGm4TIf3C01Nx+9PVwB2Z+30hhbfIEBxD4loRFmh/hU5TIQEpneF8yoIfe9EnMaoZbq86xhADZXvLIZvpXUdm1NQZDG6na2S1fwyOUKQsW6BWLcfoZZwZlrXrViD1jBsHBV++s+lxShTeTCszlo= - file: - - dist/black - skip_cleanup: true - on: - condition: $TRAVIS_PYTHON_VERSION == '3.6' - repo: psf/black - tags: true diff --git a/CHANGES.md b/CHANGES.md index dfc54224b41..240abe302a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,11 @@ - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) +#### _Packaging_ + +- Self-contained native _Black_ binaries are now provided for releases via GitHub + Releases (#1743) + ### 20.8b1 #### _Packaging_ From edf1c9dc0f20239b9d5351d8b0607c9e936dd43f Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Fri, 6 Nov 2020 19:17:23 -0500 Subject: [PATCH 091/680] Fix bug which causes f-expressions to be split (#1809) Closes #1807. --- src/black/__init__.py | 5 +++-- tests/data/long_strings__regression.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index e09838d866a..7a7456ad78b 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3611,13 +3611,14 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): MIN_SUBSTR_SIZE = 6 # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. RE_FEXPR = r""" - (? TMatchResult: diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 9bfb1eedd5a..7065b2fcef8 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -371,6 +371,10 @@ def xxxxxxx_xxxxxx(xxxx): ), } +# We do NOT split on f-string expressions. +print(f"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam. {[f'{i}' for i in range(10)]}") +x = f"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}." + # output @@ -830,3 +834,13 @@ def xxxxxxx_xxxxxx(xxxx): r"(? Date: Mon, 9 Nov 2020 14:58:23 -0500 Subject: [PATCH 092/680] Correctly handle inline tabs in docstrings (#1810) The `fix_docstring` function expanded all tabs, which caused a difference in the AST representation when those tabs were inline and not leading. This changes the function to only expand leading tabs so inline tabs are preserved. Fixes #1601. --- src/black/__init__.py | 26 +++++++++++++++++--- tests/data/docstring.py | 53 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 7a7456ad78b..7e13a5d33f5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6744,13 +6744,33 @@ def is_docstring(leaf: Leaf) -> bool: return False +def lines_with_leading_tabs_expanded(s: str) -> List[str]: + """ + Splits string into lines and expands only leading tabs (following the normal + Python rules) + """ + lines = [] + 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) + if match: + first_non_whitespace_idx = match.start(1) + + lines.append( + line[:first_non_whitespace_idx].expandtabs() + + line[first_non_whitespace_idx:] + ) + else: + lines.append(line) + return lines + + def fix_docstring(docstring: str, prefix: str) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: return "" - # Convert tabs to spaces (following the normal Python rules) - # and split into a list of lines: - lines = docstring.expandtabs().splitlines() + lines = lines_with_leading_tabs_expanded(docstring) # Determine minimum indentation (first line doesn't count): indent = sys.maxsize for line in lines[1:]: diff --git a/tests/data/docstring.py b/tests/data/docstring.py index 2d3d73a101c..5c6985d0e08 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -110,6 +110,32 @@ def ignored_docstring(): """a => \ b""" + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + # output class MyClass: @@ -222,4 +248,29 @@ def believe_it_or_not_this_is_in_the_py_stdlib(): def ignored_docstring(): """a => \ -b""" \ No newline at end of file +b""" + + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass From 7d032fa848c8910007a0a41c1ba61d70d2846f48 Mon Sep 17 00:00:00 2001 From: Casper Weiss Bang Date: Fri, 13 Nov 2020 16:25:17 +0100 Subject: [PATCH 093/680] Use lowercase hex numbers fixes #1692 (#1775) * Made hex lower case * Refactored numeric formatting section * Redid some refactoring and removed bloat * Removed additions from test_requirements.txt * Primer now expects expected changes * Undid some refactoring * added to changelog * Update src/black/__init__.py Co-authored-by: Zsolt Dollenstein Co-authored-by: Zsolt Dollenstein Co-authored-by: Cooper Lees --- .gitignore | 3 +- CHANGES.md | 2 + src/black/__init__.py | 57 +++++++++++++------ src/black_primer/primer.json | 10 ++-- src/blib2to3/pytree.py | 2 +- test_requirements.txt | 2 +- tests/data/numeric_literals.py | 4 +- tests/data/numeric_literals_py2.py | 4 +- .../data/numeric_literals_skip_underscores.py | 6 +- 9 files changed, 57 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 3207e72ae28..30225ec7764 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.venv .coverage _build .DS_Store @@ -15,4 +16,4 @@ src/_black_version.py .dmypy.json *.swp .hypothesis/ -venv/ \ No newline at end of file +venv/ diff --git a/CHANGES.md b/CHANGES.md index 240abe302a4..67697bd7b07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) +- use lowercase hex strings (#1692) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/src/black/__init__.py b/src/black/__init__.py index 7e13a5d33f5..44edeb0d9f1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5192,31 +5192,52 @@ def normalize_numeric_literal(leaf: Leaf) -> None: # Leave octal and binary literals alone. pass elif text.startswith("0x"): - # Change hex literals to upper case. - before, after = text[:2], text[2:] - text = f"{before}{after.upper()}" + text = format_hex(text) elif "e" in text: - before, after = text.split("e") - sign = "" - if after.startswith("-"): - after = after[1:] - sign = "-" - elif after.startswith("+"): - after = after[1:] - before = format_float_or_int_string(before) - text = f"{before}e{sign}{after}" + text = format_scientific_notation(text) elif text.endswith(("j", "l")): - number = text[:-1] - suffix = text[-1] - # Capitalize in "2L" because "l" looks too similar to "1". - if suffix == "l": - suffix = "L" - text = f"{format_float_or_int_string(number)}{suffix}" + text = format_long_or_complex_number(text) else: text = format_float_or_int_string(text) leaf.value = text +def format_hex(text: str) -> str: + """ + Formats a hexadecimal string like "0x12b3" + + Uses lowercase because of similarity between "B" and "8", which + can cause security issues. + see: https://github.com/psf/black/issues/1692 + """ + + before, after = text[:2], text[2:] + return f"{before}{after.lower()}" + + +def format_scientific_notation(text: str) -> str: + """Formats a numeric string utilizing scentific notation""" + before, after = text.split("e") + sign = "" + if after.startswith("-"): + after = after[1:] + sign = "-" + elif after.startswith("+"): + after = after[1:] + before = format_float_or_int_string(before) + 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`""" + 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}" + + def format_float_or_int_string(text: str) -> str: """Formats a float string like "1.0".""" if "." not in text: diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index cdc863ca032..32df01571a7 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -10,7 +10,7 @@ }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] @@ -47,7 +47,7 @@ }, "hypothesis": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] @@ -63,7 +63,7 @@ }, "pillow": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-pillow/Pillow.git", "long_checkout": false, "py_versions": ["all"] @@ -77,7 +77,7 @@ }, "pyramid": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/Pylons/pyramid.git", "long_checkout": false, "py_versions": ["all"] @@ -112,7 +112,7 @@ }, "virtualenv": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pypa/virtualenv.git", "long_checkout": false, "py_versions": ["all"] diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 4b841b768e7..6dba3c7bb15 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -34,7 +34,7 @@ import sys from io import StringIO -HUGE: int = 0x7FFFFFFF # maximum repeat count, default max +HUGE: int = 0x7fffffff # maximum repeat count, default max _type_reprs: Dict[int, Union[Text, int]] = {} diff --git a/test_requirements.txt b/test_requirements.txt index 3e65cdb669f..9f69b8edf83 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,4 +2,4 @@ pytest >= 6.1.1 pytest-mock >= 3.3.1 pytest-cases >= 2.3.0 coverage >= 5.3 -parameterized >= 0.7.4 \ No newline at end of file +parameterized >= 0.7.4 diff --git a/tests/data/numeric_literals.py b/tests/data/numeric_literals.py index 254da68d330..06b7f7758ee 100644 --- a/tests/data/numeric_literals.py +++ b/tests/data/numeric_literals.py @@ -12,7 +12,7 @@ x = 123456789E123456789 x = 123456789J x = 123456789.123456789J -x = 0XB1ACC +x = 0Xb1aCc x = 0B1011 x = 0O777 x = 0.000000006 @@ -36,7 +36,7 @@ x = 123456789e123456789 x = 123456789j x = 123456789.123456789j -x = 0xB1ACC +x = 0xb1acc x = 0b1011 x = 0o777 x = 0.000000006 diff --git a/tests/data/numeric_literals_py2.py b/tests/data/numeric_literals_py2.py index 8f85c43f265..8b2c7faa306 100644 --- a/tests/data/numeric_literals_py2.py +++ b/tests/data/numeric_literals_py2.py @@ -3,7 +3,7 @@ x = 123456789L x = 123456789l x = 123456789 -x = 0xb1acc +x = 0xB1aCc # output @@ -13,4 +13,4 @@ x = 123456789L x = 123456789L x = 123456789 -x = 0xB1ACC +x = 0xb1acc diff --git a/tests/data/numeric_literals_skip_underscores.py b/tests/data/numeric_literals_skip_underscores.py index e345bb90276..f83e23312f2 100644 --- a/tests/data/numeric_literals_skip_underscores.py +++ b/tests/data/numeric_literals_skip_underscores.py @@ -3,7 +3,7 @@ x = 123456789 x = 1_2_3_4_5_6_7 x = 1E+1 -x = 0xb1acc +x = 0xb1AcC x = 0.00_00_006 x = 12_34_567J x = .1_2 @@ -16,8 +16,8 @@ x = 123456789 x = 1_2_3_4_5_6_7 x = 1e1 -x = 0xB1ACC +x = 0xb1acc x = 0.00_00_006 x = 12_34_567j x = 0.1_2 -x = 1_2.0 \ No newline at end of file +x = 1_2.0 From dea81b7ad5cfa04c3572771c34af823449d0a8f3 Mon Sep 17 00:00:00 2001 From: Thiago Bellini Ribeiro Date: Fri, 13 Nov 2020 12:26:07 -0300 Subject: [PATCH 094/680] Provide a stdin-filename to allow stdin to respect force-exclude rules (#1780) * Provide a stdin-filename to allow stdin to respect exclude/force-exclude rules This will allow automatic tools to enforce the project's exclude/force-exclude rules even if they pass the file through stdin to update its buffer. This is a similar solution to --stdin-display-name in flake8. * Update src/black/__init__.py Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> * --stdin-filename should only respect --exclude-filename * Update README with the new --stdin-filename option * Write some tests for the new stdin-filename functionality * Apply suggestions from code review Co-authored-by: Hugo van Kemenade * Force stdin output when we asked for stdin even if the file exists * Add an entry in the changelog regarding --stdin-filename * Reduce disk reads if possible Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> * Check for is_stdin and p.is_file before checking for p.is_dir() Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Hugo van Kemenade --- README.md | 5 ++ docs/change_log.md | 3 + src/black/__init__.py | 67 +++++++++++++----- tests/test_black.py | 159 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d34ce146e0d..a557a6fc6c8 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,11 @@ Options: matching this regex will be excluded even when they are passed explicitly as arguments. + --stdin-filename TEXT 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. + -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with 2>/dev/null. diff --git a/docs/change_log.md b/docs/change_log.md index 1ee35a4d8f9..e6afefbf686 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -23,6 +23,9 @@ - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) +- Added `--stdin-filename` argument to allow stdin to respect `--force-exclude` rules. + Works very alike to flake8's `--stdin-display-name` (#1780) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 44edeb0d9f1..48690573810 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -68,6 +68,7 @@ DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 DEFAULT_INCLUDES = r"\.pyi?$" CACHE_DIR = Path(user_cache_dir("black", version=__version__)) +STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters. @@ -457,6 +458,15 @@ def target_version_option_callback( "excluded even when they are passed explicitly as arguments." ), ) +@click.option( + "--stdin-filename", + type=str, + 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." + ), +) @click.option( "-q", "--quiet", @@ -516,6 +526,7 @@ def main( include: str, exclude: str, force_exclude: Optional[str], + stdin_filename: Optional[str], src: Tuple[str, ...], config: Optional[str], ) -> None: @@ -548,6 +559,7 @@ def main( exclude=exclude, force_exclude=force_exclude, report=report, + stdin_filename=stdin_filename, ) path_empty( @@ -587,6 +599,7 @@ def get_sources( exclude: str, force_exclude: Optional[str], report: "Report", + stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" try: @@ -613,22 +626,14 @@ def get_sources( gitignore = get_gitignore(root) for s in src: - p = Path(s) - if p.is_dir(): - sources.update( - gen_python_files( - p.iterdir(), - root, - include_regex, - exclude_regex, - force_exclude_regex, - report, - gitignore, - ) - ) - elif s == "-": - sources.add(p) - elif p.is_file(): + if s == "-" and stdin_filename: + p = Path(stdin_filename) + is_stdin = True + else: + p = Path(s) + is_stdin = False + + if is_stdin or p.is_file(): normalized_path = normalize_path_maybe_ignore(p, root, report) if normalized_path is None: continue @@ -643,6 +648,23 @@ def get_sources( report.path_ignored(p, "matches the --force-exclude regular expression") continue + if is_stdin: + p = Path(f"{STDIN_PLACEHOLDER}{str(p)}") + + sources.add(p) + elif p.is_dir(): + sources.update( + gen_python_files( + p.iterdir(), + root, + include_regex, + exclude_regex, + force_exclude_regex, + report, + gitignore, + ) + ) + elif s == "-": sources.add(p) else: err(f"invalid path: {s}") @@ -670,7 +692,18 @@ def reformat_one( """ try: changed = Changed.NO - if not src.is_file() and str(src) == "-": + + if str(src) == "-": + is_stdin = True + elif str(src).startswith(STDIN_PLACEHOLDER): + is_stdin = True + # Use the original name again in case we want to print something + # to the user + src = Path(str(src)[len(STDIN_PLACEHOLDER) :]) + else: + is_stdin = False + + if is_stdin: if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode): changed = Changed.YES else: diff --git a/tests/test_black.py b/tests/test_black.py index b0cf6ed5caa..a688c8780ef 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1336,10 +1336,169 @@ def test_exclude_for_issue_1572(self) -> None: exclude=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_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=include, + exclude=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_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=include, + exclude=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_exclude(self) -> None: + # Exclude shouldn't exclude stdin_filename since it is mimicing 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=include, + exclude=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=include, + exclude="", + force_exclude=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", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + path = Path("-") + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once() + report.done.assert_called_with(path, black.Changed.YES) + + def test_reformat_one_with_stdin_filename(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + p = "foo.py" + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once() + # __BLACK_STDIN_FILENAME__ should have been striped + report.done.assert_called_with(expected, black.Changed.YES) + + def test_reformat_one_with_stdin_and_existing_path(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + 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")) + # Make sure is_file actually returns True + self.assertTrue(p.is_file()) + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once() + # __BLACK_STDIN_FILENAME__ should have been striped + report.done.assert_called_with(expected, black.Changed.YES) + def test_gitignore_exclude(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" include = re.compile(r"\.pyi?$") From 4d03716eaea58ee38d77cf2bba0a72b7a27ec9fa Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 24 Nov 2020 04:39:25 -0500 Subject: [PATCH 095/680] Allow same RHS expressions in annotated assignments as in regular assignments (#1835) --- CHANGES.md | 3 +++ src/blib2to3/Grammar.txt | 2 +- tests/data/python38.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 67697bd7b07..ca8a0472a3b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,9 @@ - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) +- Added parsing support for unparenthesized tuples and yield expressions in annotated + assignments (#1835) + - use lowercase hex strings (#1692) #### _Packaging_ diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index eafaee84cb3..69b9af96608 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -77,7 +77,7 @@ small_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))*) -annassign: ':' test ['=' test] +annassign: ':' test ['=' (yield_expr|testlist_star_expr)] testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' | '**=' | '//=') diff --git a/tests/data/python38.py b/tests/data/python38.py index 1a7f76167d9..63b0588bc27 100644 --- a/tests/data/python38.py +++ b/tests/data/python38.py @@ -11,6 +11,14 @@ def starred_yield(): yield "value1", *my_list +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" + + # output @@ -25,3 +33,13 @@ def starred_return(): def starred_yield(): my_list = ["value2", "value3"] yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" From 7f75fe3669ebf0627b1b0476a6d02047e909b959 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 25 Nov 2020 19:26:35 -0500 Subject: [PATCH 096/680] Switch back to Python 3.8 for ReadTheDocs (#1839) ReadTheDocs doesn't support Python 3.9 yet. --- readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs.yml b/readthedocs.yml index 32bcf1fa50e..15065033d0f 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,6 +1,6 @@ version: 2 python: - version: 3.9 + version: 3.8 install: - requirements: docs/requirements.txt - method: setuptools From 2989dc1bf822b1b2a6bd250cea37bbf20c237764 Mon Sep 17 00:00:00 2001 From: Noel Evans Date: Wed, 9 Dec 2020 23:40:45 +0000 Subject: [PATCH 097/680] vim plugin: Add quiet flag so non-error actions go unreported (#1733) --- docs/editor_integration.md | 1 + plugin/black.vim | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index 21a6865d60e..037b265b9a0 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -123,6 +123,7 @@ Configuration: - `g:black_linelength` (defaults to `88`) - `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`) To install with [vim-plug](https://github.com/junegunn/vim-plug): diff --git a/plugin/black.vim b/plugin/black.vim index 3dd3f2151c3..c5f0313f4ac 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -48,6 +48,9 @@ if !exists("g:black_string_normalization") let g:black_string_normalization = 1 endif endif +if !exists("g:black_quiet") + let g:black_quiet = 0 +endif python3 << EndPython3 import collections @@ -74,6 +77,7 @@ FLAGS = [ Flag(name="line_length", cast=int), Flag(name="fast", cast=strtobool), Flag(name="string_normalization", cast=strtobool), + Flag(name="quiet", cast=strtobool), ] @@ -156,6 +160,7 @@ def Black(): string_normalization=configs["string_normalization"], is_pyi=vim.current.buffer.name.endswith('.pyi'), ) + quiet = configs["quiet"] buffer_str = '\n'.join(vim.current.buffer) + '\n' try: @@ -165,7 +170,8 @@ def Black(): mode=mode, ) except black.NothingChanged: - print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)') + if not quiet: + print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)') except Exception as exc: print(exc) else: @@ -183,7 +189,8 @@ def Black(): window.cursor = cursor except vim.error: window.cursor = (len(window.buffer), 0) - print(f'Reformatted in {time.time() - start:.4f}s.') + if not quiet: + print(f'Reformatted in {time.time() - start:.4f}s.') def get_configs(): path_pyproject_toml = black.find_pyproject_toml(vim.eval("fnamemodify(getcwd(), ':t')")) From a522aa45c65bb896637d0add27b4c2a9fec6b976 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 13 Dec 2020 23:20:25 -0800 Subject: [PATCH 098/680] Remove all trace of flake8-mypy (#1867) flake8-mypy is long dead and shouldn't be used, see https://github.com/ambv/flake8-mypy. We appear to use pre-commit to run mypy now anyway. I ran `pipenv uninstall flake8-mypy`, which seems to have made several changes to Pipfile.lock. Let me know if there's a better way to do this. Co-authored-by: hauntsaninja <> --- Pipfile | 1 - Pipfile.lock | 842 +++++++++++++++++++++++++++++---------------------- mypy.ini | 1 - 3 files changed, 473 insertions(+), 371 deletions(-) diff --git a/Pipfile b/Pipfile index 1ced1ed096b..ba596b3d738 100644 --- a/Pipfile +++ b/Pipfile @@ -9,7 +9,6 @@ coverage = "*" docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 flake8 = "*" flake8-bugbear = "*" -flake8-mypy = "*" mypy = ">=0.782" pre-commit = "*" readme_renderer = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 95b7613114a..a5c38aa0777 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "46390803c9b9e1b77a1b4a29de602d864dea188488d3aee6361030c91529611c" + "sha256": "21836c0a63b6e3e1eacd0adec7dea61d2d5989e38225edd976ff144e499f0426" }, "pipfile-spec": 6, "requires": {}, @@ -16,21 +16,46 @@ "default": { "aiohttp": { "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", + "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", + "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", + "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", + "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", + "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", + "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", + "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", + "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", + "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", + "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", + "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", + "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", + "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", + "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", + "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", + "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", + "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", + "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", + "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", + "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", + "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", + "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", + "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", + "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", + "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", + "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", + "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", + "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", + "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", + "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", + "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", + "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", + "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", + "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", + "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", + "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" ], "index": "pypi", - "version": "==3.6.2" + "version": "==3.7.3" }, "aiohttp-cors": { "hashes": [ @@ -53,14 +78,16 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "black": { "editable": true, @@ -90,36 +117,60 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "version": "==0.6" + "python_version <": "3.7", + "version": "==0.6", + "version >": "0.6" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "multidict": { "hashes": [ - "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", - "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", - "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", - "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", - "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", - "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", - "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", - "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", - "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", - "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", - "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", - "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", - "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", - "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", - "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", - "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", - "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" - ], - "version": "==4.7.6" + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "mypy-extensions": { "hashes": [ @@ -131,46 +182,80 @@ }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], "index": "pypi", - "version": "==0.8.0" + "version": "==0.8.1" }, "regex": { "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" ], "index": "pypi", - "version": "==2020.7.14" + "version": "==2020.11.13" + }, + "setuptools-scm": { + "hashes": [ + "sha256:1fc4e25df445351d172bb4788f4d07f9e9ce0e8b7dee6b19584e46110172ca13", + "sha256:48b31488d089270500f120efea723968c01abd85fd4876043a3b7c7ef7d0b761", + "sha256:62fa535edb31ece9fa65dc9dcb3056145b8020c8c26c0ef1018aef33db95c40d", + "sha256:b928021c4381f96d577847d540d6e03065f8f8851c768a0c9bc552d463bae0d4", + "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a", + "sha256:db4ab2e0c2644ba71b1e5212b14ff65dbf0af465796d314a75e0cf6128f605f7", + "sha256:e878036c2527dfd05818727846338be86b2be6955741b85fab2625f63d001021", + "sha256:eb19b46816fc106a4c2d4180022687eab40f9773cf61390b845afb093d1f4ecd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.0.1" }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "index": "pypi", - "version": "==0.10.1" + "version": "==0.10.2" }, "typed-ast": { "hashes": [ @@ -187,6 +272,7 @@ "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", @@ -198,8 +284,10 @@ "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "index": "pypi", @@ -216,61 +304,91 @@ }, "yarl": { "hashes": [ - "sha256:03b7a44384ad60be1b7be93c2a24dc74895f8d767ea0bce15b2f6fc7695a3843", - "sha256:076157404db9db4bb3fa9db22db319bbb36d075eeab19ba018ce20ae0cacf037", - "sha256:1c05ae3d5ea4287470046a2c2754f0a4c171b84ea72c8a691f776eb1753dfb91", - "sha256:2467baf8233f7c64048df37e11879c553943ffe7f373e689711ec2807ea13805", - "sha256:2bb2e21cf062dfbe985c3cd4618bae9f25271efcad9e7be1277861247eee9839", - "sha256:311effab3b3828ab34f0e661bb57ff422f67d5c33056298bda4c12195251f8dd", - "sha256:3526cb5905907f0e42bee7ef57ae4a5f02bc27dcac27859269e2bba0caa4c2b6", - "sha256:39b1e586f34b1d2512c9b39aa3cf24c870c972d525e36edc9ee19065db4737bb", - "sha256:4bed5cd7c8e69551eb19df15295ba90e62b9a6a1149c76eb4a9bab194402a156", - "sha256:51c6d3cf7a1f1fbe134bb92f33b7affd94d6de24cd64b466eb12de52120fb8c6", - "sha256:59f78b5da34ddcffb663b772f7619e296518712e022e57fc5d9f921818e2ab7c", - "sha256:6f29115b0c330da25a04f48612d75333bca04521181a666ca0b8761005a99150", - "sha256:73d4e1e1ef5e52d526c92f07d16329e1678612c6a81dd8101fdcae11a72de15c", - "sha256:9b48d31f8d881713fd461abfe7acbb4dcfeb47cec3056aa83f2fbcd2244577f7", - "sha256:a1fd575dd058e10ad4c35065e7c3007cc74d142f622b14e168d8a273a2fa8713", - "sha256:b3dd1052afd436ba737e61f5d3bed1f43a7f9a33fc58fbe4226eb919a7006019", - "sha256:b99c25ed5c355b35d1e6dae87ac7297a4844a57dc5766b173b88b6163a36eb0d", - "sha256:c056e86bff5a0b566e0d9fab4f67e83b12ae9cbcd250d334cbe2005bbe8c96f2", - "sha256:c45b49b59a5724869899798e1bbd447ac486215269511d3b76b4c235a1b766b6", - "sha256:cd623170c729a865037828e3f99f8ebdb22a467177a539680dfc5670b74c84e2", - "sha256:d25d3311794e6c71b608d7c47651c8f65eea5ab15358a27f29330b3475e8f8e5", - "sha256:d695439c201ed340745250f9eb4dfe8d32bf1e680c16477107b8f3ce4bff4fdb", - "sha256:d77f6c9133d2aabb290a7846aaa74ec14d7b5ab35b01591fac5a70c4a8c959a2", - "sha256:d894a2442d2cd20a3b0b0dce5a353d316c57d25a2b445e03f7eac90eee27b8af", - "sha256:db643ce2b58a4bd11a82348225c53c76ecdd82bb37cf4c085e6df1b676f4038c", - "sha256:e3a0c43a26dfed955b2a06fdc4d51d2c51bc2200aff8ce8faf14e676ea8c8862", - "sha256:e77bf79ad1ccae672eab22453838382fe9029fc27c8029e84913855512a587d8", - "sha256:f2f0174cb15435957d3b751093f89aede77df59a499ab7516bbb633b77ead13a", - "sha256:f3031c78edf10315abe232254e6a36b65afe65fded41ee54ed7976d0b2cdf0da", - "sha256:f4c007156732866aa4507d619fe6f8f2748caabed4f66b276ccd97c82572620c", - "sha256:f4f27ff3dd80bc7c402def211a47291ea123d59a23f59fe18fc0e81e3e71f385", - "sha256:f57744fc61e118b5d114ae8077d8eb9df4d2d2c11e2af194e21f0c11ed9dcf6c", - "sha256:f835015a825980b65356e9520979a1564c56efea7da7d4b68a14d4a07a3a7336" - ], - "version": "==1.6.2" + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==1.6.3" } }, "develop": { "aiohttp": { "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", + "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", + "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", + "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", + "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", + "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", + "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", + "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", + "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", + "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", + "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", + "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", + "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", + "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", + "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", + "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", + "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", + "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", + "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", + "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", + "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", + "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", + "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", + "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", + "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", + "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", + "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", + "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", + "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", + "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", + "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", + "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", + "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", + "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", + "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", + "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", + "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" ], "index": "pypi", - "version": "==3.6.2" + "version": "==3.7.3" }, "aiohttp-cors": { "hashes": [ @@ -300,21 +418,24 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "babel": { "hashes": [ - "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", - "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "version": "==2.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0" }, "black": { "editable": true, @@ -328,61 +449,22 @@ "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.2.1" }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" - ], - "version": "==2020.6.20" - }, - "cffi": { - "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "version": "==1.14.3" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -405,6 +487,7 @@ "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" }, "commonmark": { @@ -454,34 +537,6 @@ "index": "pypi", "version": "==5.3" }, - "cryptography": { - "hashes": [ - "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", - "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", - "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", - "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", - "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", - "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", - "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", - "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", - "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", - "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", - "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", - "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", - "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", - "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", - "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", - "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", - "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", - "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", - "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", - "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", - "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", - "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" - ], - "index": "pypi", - "version": "==3.2" - }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -492,6 +547,7 @@ "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", + "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -506,40 +562,34 @@ }, "flake8": { "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], "index": "pypi", - "version": "==3.8.3" + "version": "==3.8.4" }, "flake8-bugbear": { "hashes": [ - "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63", - "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162" - ], - "index": "pypi", - "version": "==20.1.4" - }, - "flake8-mypy": { - "hashes": [ - "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18", - "sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc" + "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538", + "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703" ], "index": "pypi", - "version": "==17.8.0" + "version": "==20.11.1" }, "identify": { "hashes": [ - "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", - "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" + "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", + "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e" ], - "version": "==1.5.6" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.5.10" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -547,29 +597,24 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "keyring": { "hashes": [ - "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", - "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + "sha256:12de23258a95f3b13e5b167f7a641a878e91eab8ef16fafc077720a95e6115bb", + "sha256:207bd66f2a9881c835dad653da04e196c678bf104f8252141d2d3c4f31051579" ], - "version": "==21.4.0" + "markers": "python_version >= '3.6'", + "version": "==21.5.0" }, "markupsafe": { "hashes": [ @@ -607,6 +652,7 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -618,45 +664,66 @@ }, "multidict": { "hashes": [ - "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", - "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", - "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", - "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", - "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", - "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", - "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", - "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", - "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", - "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", - "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", - "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", - "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", - "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", - "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", - "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", - "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" - ], - "version": "==4.7.6" + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "mypy": { "hashes": [ - "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", - "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", - "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", - "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", - "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", - "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", - "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", - "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", - "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", - "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", - "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", - "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", - "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", - "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" + "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324", + "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc", + "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802", + "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122", + "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975", + "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7", + "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666", + "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669", + "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178", + "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01", + "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea", + "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de", + "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1", + "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c" ], "index": "pypi", - "version": "==0.782" + "version": "==0.790" }, "mypy-extensions": { "hashes": [ @@ -675,18 +742,19 @@ }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", + "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" ], - "version": "==20.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.8" }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], "index": "pypi", - "version": "==0.8.0" + "version": "==0.8.1" }, "pkginfo": { "hashes": [ @@ -697,64 +765,63 @@ }, "pre-commit": { "hashes": [ - "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", - "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" + "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", + "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4" ], "index": "pypi", - "version": "==2.7.1" + "version": "==2.9.3" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { "hashes": [ - "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", - "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" + "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", + "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" ], - "version": "==2.7.2" + "markers": "python_version >= '3.5'", + "version": "==2.7.3" }, "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" }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", + "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" ], - "version": "==2020.1" + "version": "==2020.4" }, "pyyaml": { "hashes": [ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" @@ -763,11 +830,11 @@ }, "readme-renderer": { "hashes": [ - "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", - "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" + "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", + "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" ], "index": "pypi", - "version": "==26.0" + "version": "==28.0" }, "recommonmark": { "hashes": [ @@ -779,37 +846,58 @@ }, "regex": { "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" ], "index": "pypi", - "version": "==2020.7.14" + "version": "==2020.11.13" }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" ], - "version": "==2.24.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.25.0" }, "requests-toolbelt": { "hashes": [ @@ -825,27 +913,26 @@ ], "version": "==1.4.0" }, - "secretstorage": { - "hashes": [ - "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", - "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.1.2" - }, "setuptools-scm": { "hashes": [ - "sha256:69258e2eeba5f7ce1ed7a5f109519580fa3578250f8e4d6684859f86d1b15826", - "sha256:a8994582e716ec690f33fec70cca0f85bd23ec974e3f783233e4879090a7faa8" + "sha256:1fc4e25df445351d172bb4788f4d07f9e9ce0e8b7dee6b19584e46110172ca13", + "sha256:48b31488d089270500f120efea723968c01abd85fd4876043a3b7c7ef7d0b761", + "sha256:62fa535edb31ece9fa65dc9dcb3056145b8020c8c26c0ef1018aef33db95c40d", + "sha256:b928021c4381f96d577847d540d6e03065f8f8851c768a0c9bc552d463bae0d4", + "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a", + "sha256:db4ab2e0c2644ba71b1e5212b14ff65dbf0af465796d314a75e0cf6128f605f7", + "sha256:e878036c2527dfd05818727846338be86b2be6955741b85fab2625f63d001021", + "sha256:eb19b46816fc106a4c2d4180022687eab40f9773cf61390b845afb093d1f4ecd" ], - "index": "pypi", - "version": "==4.1.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.0.1" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -857,17 +944,18 @@ }, "sphinx": { "hashes": [ - "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", - "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0" + "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300", + "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.3.1" }, "sphinxcontrib-applehelp": { "hashes": [ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -875,6 +963,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -882,6 +971,7 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -889,6 +979,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -896,6 +987,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -903,22 +995,24 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], + "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "index": "pypi", - "version": "==0.10.1" + "version": "==0.10.2" }, "tqdm": { "hashes": [ - "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", - "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" + "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5", + "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1" ], - "version": "==4.51.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.54.1" }, "twine": { "hashes": [ @@ -943,6 +1037,7 @@ "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", @@ -954,8 +1049,10 @@ "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "index": "pypi", @@ -972,17 +1069,19 @@ }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], - "version": "==1.25.11" + "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.2" }, "virtualenv": { "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", + "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" ], - "version": "==20.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.2" }, "webencodings": { "hashes": [ @@ -993,49 +1092,54 @@ }, "wheel": { "hashes": [ - "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2", - "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f" + "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e", + "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e" ], "index": "pypi", - "version": "==0.35.1" + "version": "==0.36.2" }, "yarl": { "hashes": [ - "sha256:03b7a44384ad60be1b7be93c2a24dc74895f8d767ea0bce15b2f6fc7695a3843", - "sha256:076157404db9db4bb3fa9db22db319bbb36d075eeab19ba018ce20ae0cacf037", - "sha256:1c05ae3d5ea4287470046a2c2754f0a4c171b84ea72c8a691f776eb1753dfb91", - "sha256:2467baf8233f7c64048df37e11879c553943ffe7f373e689711ec2807ea13805", - "sha256:2bb2e21cf062dfbe985c3cd4618bae9f25271efcad9e7be1277861247eee9839", - "sha256:311effab3b3828ab34f0e661bb57ff422f67d5c33056298bda4c12195251f8dd", - "sha256:3526cb5905907f0e42bee7ef57ae4a5f02bc27dcac27859269e2bba0caa4c2b6", - "sha256:39b1e586f34b1d2512c9b39aa3cf24c870c972d525e36edc9ee19065db4737bb", - "sha256:4bed5cd7c8e69551eb19df15295ba90e62b9a6a1149c76eb4a9bab194402a156", - "sha256:51c6d3cf7a1f1fbe134bb92f33b7affd94d6de24cd64b466eb12de52120fb8c6", - "sha256:59f78b5da34ddcffb663b772f7619e296518712e022e57fc5d9f921818e2ab7c", - "sha256:6f29115b0c330da25a04f48612d75333bca04521181a666ca0b8761005a99150", - "sha256:73d4e1e1ef5e52d526c92f07d16329e1678612c6a81dd8101fdcae11a72de15c", - "sha256:9b48d31f8d881713fd461abfe7acbb4dcfeb47cec3056aa83f2fbcd2244577f7", - "sha256:a1fd575dd058e10ad4c35065e7c3007cc74d142f622b14e168d8a273a2fa8713", - "sha256:b3dd1052afd436ba737e61f5d3bed1f43a7f9a33fc58fbe4226eb919a7006019", - "sha256:b99c25ed5c355b35d1e6dae87ac7297a4844a57dc5766b173b88b6163a36eb0d", - "sha256:c056e86bff5a0b566e0d9fab4f67e83b12ae9cbcd250d334cbe2005bbe8c96f2", - "sha256:c45b49b59a5724869899798e1bbd447ac486215269511d3b76b4c235a1b766b6", - "sha256:cd623170c729a865037828e3f99f8ebdb22a467177a539680dfc5670b74c84e2", - "sha256:d25d3311794e6c71b608d7c47651c8f65eea5ab15358a27f29330b3475e8f8e5", - "sha256:d695439c201ed340745250f9eb4dfe8d32bf1e680c16477107b8f3ce4bff4fdb", - "sha256:d77f6c9133d2aabb290a7846aaa74ec14d7b5ab35b01591fac5a70c4a8c959a2", - "sha256:d894a2442d2cd20a3b0b0dce5a353d316c57d25a2b445e03f7eac90eee27b8af", - "sha256:db643ce2b58a4bd11a82348225c53c76ecdd82bb37cf4c085e6df1b676f4038c", - "sha256:e3a0c43a26dfed955b2a06fdc4d51d2c51bc2200aff8ce8faf14e676ea8c8862", - "sha256:e77bf79ad1ccae672eab22453838382fe9029fc27c8029e84913855512a587d8", - "sha256:f2f0174cb15435957d3b751093f89aede77df59a499ab7516bbb633b77ead13a", - "sha256:f3031c78edf10315abe232254e6a36b65afe65fded41ee54ed7976d0b2cdf0da", - "sha256:f4c007156732866aa4507d619fe6f8f2748caabed4f66b276ccd97c82572620c", - "sha256:f4f27ff3dd80bc7c402def211a47291ea123d59a23f59fe18fc0e81e3e71f385", - "sha256:f57744fc61e118b5d114ae8077d8eb9df4d2d2c11e2af194e21f0c11ed9dcf6c", - "sha256:f835015a825980b65356e9520979a1564c56efea7da7d4b68a14d4a07a3a7336" - ], - "version": "==1.6.2" + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==1.6.3" } } } diff --git a/mypy.ini b/mypy.ini index 295bab4e302..589fbf65139 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,6 @@ python_version=3.6 platform=linux -# flake8-mypy expects the two following for sensible formatting show_column_numbers=True # show error messages from unrelated files From c3df2c1a69c687d34f41478235c869d0526a46bd Mon Sep 17 00:00:00 2001 From: nikkie Date: Mon, 28 Dec 2020 00:18:35 +0900 Subject: [PATCH 099/680] fix format_str() docstring to prevent users from running into NameError (#1885) --- 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 48690573810..5f0f89719f9 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -986,7 +986,7 @@ def format_str(src_contents: str, *, mode: Mode) -> FileContent: allowed. Example: >>> import black - >>> print(black.format_str("def f(arg:str='')->None:...", mode=Mode())) + >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode())) def f(arg: str = "") -> None: ... From e7ddcb8686859cf3033b2a5d1a2934426abdae9d Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 27 Dec 2020 10:19:08 -0500 Subject: [PATCH 100/680] Fuzz on Python 3.9 too (#1882) Fuzzing on Python 3.9 used to cause errors but now they have disappeared on more modern Python 3.9 and Hypothesmith. --- .github/workflows/fuzz.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 343eed10df8..0153767509a 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] # Python3.9 should be added after fixing [https://github.com/Zac-HD/hypothesmith/issues/11]. + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 From a497570fcb364b40e0d952d3133e8d4f2d329fea Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 27 Dec 2020 21:38:11 -0500 Subject: [PATCH 101/680] Bump mypy to 0.780 in pre-commit config (#1887) To avoid hitting a mypy bug causes pre-commit to always fail on CPython 3.9. Even though it's still an outdated version, the bug effectively blocks development on CPython 3.9 so that's why this commit exists instead of waiting for cooperlees to finish his bump to 0.790 PR. Also this fixes primer to ensure it always raises CalledProcessError with an int error code. I stole the patch from cooperlees's mypy bump PR. It's funny how mypy 0.790 is already asked for in our Pipfile.lock file, but oh well mypy is probably more commonly run through pre-commit than standalone I guess. Oh and if you're curious why the bug doesn't up on CPython 3.8 or lower: there was some subscription AST changes in CPython 3.9. --- .pre-commit-config.yaml | 2 +- src/black_primer/lib.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 667b22d6328..4e12e46f8d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.770 + rev: v0.780 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index afeb0721cc4..5c5576e1ff3 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -59,10 +59,12 @@ async def _gen_check_output( raise if process.returncode != 0: + returncode = process.returncode + if returncode is None: + returncode = 69 + cmd_str = " ".join(cmd) - raise CalledProcessError( - process.returncode, cmd_str, output=stdout, stderr=stderr - ) + raise CalledProcessError(returncode, cmd_str, output=stdout, stderr=stderr) return (stdout, stderr) From e912c7ff54c392e92a765c5eff0bd2ca3bb05b47 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Mon, 28 Dec 2020 15:30:23 -0500 Subject: [PATCH 102/680] Fix INTERNAL ERROR caused by removing parens from pointless string (#1888) Fixes #1846. --- src/black/__init__.py | 11 ++++++- tests/data/long_strings__regression.py | 42 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 5f0f89719f9..c7c5d724a9f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3271,7 +3271,8 @@ class StringParenStripper(StringTransformer): Requirements: The line contains a string which is surrounded by parentheses and: - - The target string is NOT the only argument to a function call). + - The target string is NOT the only argument to a function call. + - The target string is NOT a "pointless" string. - If the target string contains a PERCENT, the brackets are not preceeded or followed by an operator with higher precedence than PERCENT. @@ -3295,6 +3296,14 @@ def do_match(self, line: Line) -> TMatchResult: if leaf.type != token.STRING: continue + # If this is a "pointless" string... + if ( + leaf.parent + and leaf.parent.parent + and leaf.parent.parent.type == syms.simple_stmt + ): + continue + # Should be preceded by a non-empty LPAR... if ( not is_valid_index(idx - 1) diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 7065b2fcef8..2e7f2483b63 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -375,6 +375,27 @@ def xxxxxxx_xxxxxx(xxxx): print(f"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam. {[f'{i}' for i in range(10)]}") x = f"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}." +# The parens should NOT be removed in this case. +( + "my very long string that should get formatted if I'm careful to make sure it goes" + " over 88 characters which it has now" +) + +# The parens should NOT be removed in this case. +( + "my very long string that should get formatted if I'm careful to make sure it goes over 88 characters which" + " it has now" +) + +# The parens should NOT be removed in this case. +( + "my very long string" + " that should get formatted" + " if I'm careful to make sure" + " it goes over 88 characters which" + " it has now" +) + # output @@ -844,3 +865,24 @@ def xxxxxxx_xxxxxx(xxxx): "This is a long string which contains an f-expr that should not split" f" {{{[i for i in range(5)]}}}." ) + +# The parens should NOT be removed in this case. +( + "my very long string that should get formatted if I'm careful to make sure it goes" + " over 88 characters which it has now" +) + +# The parens should NOT be removed in this case. +( + "my very long string that should get formatted if I'm careful to make sure it goes" + " over 88 characters which it has now" +) + +# The parens should NOT be removed in this case. +( + "my very long string" + " that should get formatted" + " if I'm careful to make sure" + " it goes over 88 characters which" + " it has now" +) From d34eb7fea38384cd1cd029179298116fabdbccd4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 31 Dec 2020 19:03:39 +0200 Subject: [PATCH 103/680] As long as it's black (#1893) Make background transparent for dark mode --- docs/_static/logo2-readme.png | Bin 80754 -> 99591 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/logo2-readme.png b/docs/_static/logo2-readme.png index ec5dafb64958d222ecbe9a29e9e13963acc6d73d..7704ec01ed98a803f59465020422ff6cd24c0958 100644 GIT binary patch literal 99591 zcmeFYj0ef?3thkKk(7k)Wwj( z!`8;mncqW*@?U501E2pn45Fm?*C{U6LX?_viWFk@PNo!GtemWDl)`8f6cmC^CT9G~ zU%vjIp99~7C@oxE9QZ*XcXxMIcMev2Cvy-xA0Hox?IY;pM;71=7H3a87efygJ7=nY zU*td6`C{s9>}2WSVrg$j@y~S)jqF`rgeWQhxzT@L{~o7{rP=@PWas>UrUgt8^v{1l z?5u2{|6UvTso+0H`4yZjO@TZAbA4fU!GE3k|2p=6?js2L=jQ*{VE#SRe;oy;DvTxw z`tPy{qseyDX+3)$q$~ABRMlhQaM?E0)ZF*k-*k)L>Wg_sWtGa(RFVx3lLWYxwr?NI z8Z@=(3lk|-wJjzlm3Ko7{>t?FFa%TLPArey9dx)Kk1Wlnk(%$Au9_tr9WH124vx{b z3AANk<0Pb}&UoAk?kt@-A+n;vqPW63$?y3PJHk7eJD5u%Pdg*y_Za`$)Bpd^|A$sU zB#&B3pJ4sctpEQ@?{BcO z1>BE~4Ifp^L75MAY!BtwkT)k+h7TfH0!+#01m9<&@eKmERm>|9z|d7Q$AVBGl% z{*vfvUF~rA(|u>Z)O=!{JBsi+9i&^uhijRSr`&^?N-X^h;YJ$OFEgcDn8vic#lvs-LIW!628+t96I@Xzh((?UgzzR zs^jBh*h8EInWuAejC*8cq@>?-VD$HFSq0Mv#>!jky%Fxhe}5t)I-VZyL~x7zYg<}w zV#>?QpK3fDY8tAm9WH(bAX}amF*zpE1%_MJN#brT?rd-Cvu*F}3|qKTjx=o#-{?q` zr`k904_B(MI#y=;Mi^i5`j;PMZuz*k-K7kF*Pv;cO|m$UZygPG?(-&E?^|DA-ya$p z(ug4!c?<@I>qGkswa2;nU~@15F}J+@Fdu}91=FuZ2S(~K8aAWMepA!X;A$RvmBsJ6 zkA+1pd=F=0VghUH>u1|FXk+nYa14&fhJK~8{FQ21FdUU;Up^K4p4EA;wlTNNc#>s>j#CR~`1sBtx+hO3uXDvhj>E=J{pB;S z=K$*wuI}h~fF8iBo^*Y-y@Y}B{0;wIn%SDaY}d*4s|tAeySTbu*e9o?AhIA3FeD?R zjgZOLjEF<3Lmx{?X1@VD%8Q0CNeyK*cu!ZKfh&W?b!aGe$f0%HX569Oj2#111!GJZ zW~^|5wZiOI&D<~4>z?lGgznt1o-Z8;niAg%3kaR`ARZn zVMXN1RtksRgVx2^@1{pvjwa#p5gFU7mAjGqP&mh#Sag=>+oQBJ%lr3;tIGUGN@(ST zluehWlBYg<-EqM&ePS=5r`9@$D_dnI+5CjhKu8C2vtYWKc=gH7gn~o z;n0~trvUQ@mY-wwew(Lr$*MuVck{EXtem_&t;z@5V%j&#pA1i-*)E#2&VI!%ESxQF4$~xP(uih=mO)II6iZvwmf)u!Qs2=s z4R75J3Ewz5j*a=LB<-1)t`3%-a`2B^=1tCqt?Ze&>FM=rWZMc}O}kv`7BgMdr+X|N z=(`DZj@2}*+^oPRu11R5#`UCJ@O&$IxdmYEPkX@Sd`pFkJ&qjpI`mhwA&{r5nVA`d zY+>J9=ryTg%|Gz6>e)%~9J~tbFVVLA_!lt3PG)9il4!h7?1UWr{K=6~QFwzO!`*4U5WLUyh!^@($GFs-oBJ1WxJxrC9h!3+VQ;X;xIccQKZY(NBg2wpyx4I00V`9M%;#^{W^M?XG*rJMUAN6m z==Iel9e!I|+h9*FOO2e4j*i~ZaTGA_^Ujakpw@BlNOnh1S@LB-P0bweW*N5o6>s6)(Q0xVi%pY zYDmT+5+<7*EU@cG)wKLG8aO!1akSx#Ny4VV>MhU;i*))i6}t>(#x`17+Nspc z-IZiD#-zcxlasKIaqkv{1O+oO-n=Pf|M)RpjX{ku7?XrImevxJTsZTq>=(UehYu}Y z7t&5!r(2stNyC zI4Hl)D&y1V$HmQ`oXvDzT8@s64%P>QpMKq(?fBF9HQfgTx%yiQJ#5O{-Tmt51ZDQ0 ztY^cWk+L3hI`NlE@DY2wJoc1Yk*}SZSesioj;6M+|IE^KR~PJJFEbv8)s(S|Q7N-C zoa?rCBM=6?cRnr4j+>~-%cAP(v0SIJcP27~q|$3?YD!YkDi{S%%-5MG9335Pu8+x| zUgIbfNR)}69rm!dUujRH?Euc8eFl+E0H=UN zGKCYaZ5EE)oX&SAOaOf1YgaSGB$r(@&MAZa#y#6TTaolg@Ty+h&ZuHn1}Do;g>*7) z)cw}JVRie#p?#8|+_&EPDLZwS%q)$re#i<|0qOH4j_PYDEG(?Dv9t4ApDdI?M9xG# zz5`a~Q(A?WUfZ(Y+2!S>J(-Z#xpSquHP_=Oav~!6m#C;y)Qa%xmLbLJ$6a#TS-g!b z-@dym@&i7EhofxTtAVCoYH^#zpFa&g@^Y$Dvl?xwJ}RGWZEe*nGpb+zdZ9*=?*6dQ zu&EuInmU9-FS7R~ntDz~guBnZ^KN;wFX~|Ltleo#+G+bd?Qca)(YT=cFc1w1!2@LF zEk>WMEiBj$rYsAW4L*_-v3IE*Rk6RGSq5M2_iQxnUTx_4y8E<0-LbF0`}^x2`ufAE zbr0cUL1KHCKt{0wQUydIi~j;JHG$6EEN#G-?m1~H?9fH7SMmLJpSrrcyE~?)8u}%= zvF^K4F^Jv|#H-gh(Nw*Y^4E+>#*@wZqv-i-^QWXPLAliFQGOCyYWuAvwMb)ahcXS0 z2pwk4kchhnHSw(bsxR^&%=fP697Z|8#>10R0fo{W92^YpO&xItyxj<+%clQP^T4Sg z6^AQEWK5&s1JNXbHFx-O&%g4ez0BZc=2Qsp6YjM4pY8v_|9S$7_%u-^;M+b~HRUR_ z8dFgf<8pf|>5gK+5An4>eQ1q7QhnWL+auVhA1s&E1Jm?i z0j3#)nCEjloHN(1ob|X4xc5=&;AES-2ofUxp@!K@L`cZG9m3RevVQC0u_F1@=;6ap zTAJ<5OFYY?jd?*tLL%*-+8-Jg^d30~*)n5Y_=IN7u|(5HV{&h*c*4i-(1*gI=o(!;x`uV`DsEYc3u}vUY_J96lPyGIti-BEL&xhdRkE zFTXfAa6``6m9!p=hwilzg4kL#z<_bb+Z&HjsIZ<)T< z`u>=@*^fDHf5X1+Q^6UAm^bZOVHC9Ztjq7YPyyqa=h9Th&5gD6af23HtvLE`_ZSnpJ zlLXUOw#0sa5c3Y6(OuF4MxUALPrCm>P>V;G_TOdVej@?nPx^D*hJYRY!xYH)oHkWP z&5ZdUJ)(PZVJA`cs`2VUmcPDCuC0ACUnuP!+CfF-$D>QKEMB0hnjMN6XHq$}^o!O^ zd&g9|jcm6HxUxEaotovFdKBc5!+|8K-9LA2KrnRh^x82;r$I4grT0hl{7O z$Ng-~Vucm{Rlg_y@CY?WLAwOCv+Xp28__)^cc@qhrqXY_UmtHZVwP12zDLVrW`((Z z+-IA)NkgN5^5ec`PWY6`bYnh^*M%f*b39`qrc9`)?nPU2d0?{=4+ zLIxNi2Jx!bW>QkG331G(7>nbSO^?zWa3T94Lf#cw_m3xSZZ~4{*oyaeh)%jKc}C=u z0MZLZiHaY@A5B8{S326f_DhSW6BLB^EccF0Z*|MO_2D-}(LlUZ1i`K;8$c+-RTb3m zvE?>*xVHWh>dBe6oqKZ~<;rXTGO7jq*Z#iM#f%16>6vFFO_ifjbpI(ekGAuD5?zeQ zUDMW>=CN0QYYN+Ol=bD3qW0GpXBAp?>J@rzm#a~3q8ODgKW3o)cM$?|#=aShX7Jd< z%;sxNk&DMuVZ`W@9!k1+DY!Uszsl(PM^?9zPcEG9G>=cBO}EAz0~-|0YRl-!G{nWl z3)lkIk)K!ZjAaz7S85l!9WP0JM+r2CgxT5Cl{G)VX*3wd`{?%5c<2Y_CcH_NlS@ofE*nlj%GSV1UXo{sU&UPCwsP zH(H|8*=j?vA*R!%W=G?hGBqaGo;N%NEC(!2OP?>EpIYz!&b2z6tNC*1F|(1*Wjzfr zCWYpwOicLKTm5u}zeY_eWR={vk1rl%gAVOdA_Y=x-Lr z+I4<0O4`TE%D#xsNzq|Q)pUevcqVO&gIVemCzN zTPG){>D!BgL0hbX^^<0vecw%&wv50WWF=G{?O(In)dtUd1A68*PF@DSFHoQNu+E%p zZs@a46VCQ*PoW(_795kgVAof-8hvf*w#4d_UtJw5$*G(1(RAoaCYjkYzo@84h}%C_ zOY>j8_-~h#`X&%lQ_@jUR<_jhtx{N%eH5_js__4r?_4l0aN zupd`4GSap@v>!Kk{87=hrotsF*^T=!8f0{L#lK7{L@koVzQkTxx#zL02 za;^gw7po0>2ILR`;H33FpOBa_P0=f2ZP|QBnUljAI20N+n2gTa{Owh;*=YKNebXPW zsUkV4q!&G&wm)`qth#v|UJN1|mAD?vO3k7ruj?OZAM?!)$q##u^DB6DCOx!g3l9x? z{@}8l{KcwJXZC|jT!>SKktut_OG+bzqGb#~vy{WJuoZj@;&!Ypq6PF{lucAi#HbkW z4X);G7aQlH5sD&Ndv@jZRO|MgN}ZLZH(<(!YGgXxXwJtsOce2@zm?^aQccvgO8eit z_jyT$xvg@U$I2L&-e8QQaAIkq4L2KxLb%MwGP_xH8kek1>e5Dj|Ic#&_5+;T4k)Cx zmX(!NgolTNwF0`()i7UWO|JJgh!_)wLMN3udAzg92_#v@US6e&1))f?%8fd*(*|bu|?{+Bp>s4Dvi{3`g<8uZi)3=h0 zdXC?{DZrl=e&(bbSk zuju3W$6F{~5eFo$x7zGZGqf=Vj`W-zH?B1z<@Oce6SwgMz6#RDw8et=%4hMHHm!Kc z_+I}BQ6x|T2QbmEtBUrbe76b{mIsXtagWZWM>ThfQQ8OTb+YC_1S~M3Y#WnpD9?R{pOg7eIAG(rI%$BIPh2 z8wojm{v9ur+F4Xvb`vkH5cVlE)8j2M?zo`?9@*?QjjD-Atjbp8Slo;r6*z5 z+cS~*-~FVwWa%}Wv?j=dx+qFlU*4O3Qhrb(CAxrD>hMrzE;QQ89Bt2FF3HO}I1;bx z+8cKqIsW20$0`Vb1@4GCb#c{O!=anmuf+H|A8)P0HC%x>UjK0BQiAROctb#$+ytL4 zDU?Z$UBGzZ#+hDxdU{#{#?#tZ&ZNrZdwv$oSA}aiu6Y%{eo4}ECKtyA|Ev4G(d`w2 z*G+e+!{_Fd!2Nj1vnE#S|K5?nngMHNJ-vsYk!c*oy-_F>1JtyYByb@RlcIQVx;Hs~ z+O7TY7M27vZ<^|-1%{@ScA`W#t9q}k^~sJpm$@ac`u zrM!m**q5s%GQh(i@U=v9{h##X_`YK+SzWfUE;NRA^waJmefNu*BefJ(f8~eU3z~*8 zV*7T>IbvMogQKQZb*=n5RX5&@{baq$C1?hT7zX3}jBoW)K_*kL`xh2O9v(jDi{Q@Q zVx);@Dv_6N=^S&S>)y~ah8HtuEGmMdhtosa$_vpVH@}n|t$HJbE@<~xt&r;nhe1!` zLic}r3Mv9Ka##FzQ7v}7(gliE9$U+)%IiN_eIPw1Cs8E#J(`pO(U_8ia$(ym3I`kp z%TO{WP@K_atbM+Hg&o7|qL=_glt@X-SecE}zKOYh670I|l;+v-lGT<)82wHAz)b(O zkg_SErLmnTB%R-M=no!~db{nB6}?IFzY*6zQU1RCxinJ`|0s~8iSTzPmUOi>Y0K4& z$QViOFXTrq1VOwtno(c#8R6G2arNd*{|?WC8DSw}q2JDrf<5O~%l+x_j4NEv zoH#J(YHv#74H0Lks)wD4*Lc*QE8fF<6z6X&wAR-Zh?rF zXcPv&Vg)MS2lkfrqXqn8ufPRKdy&j1*Ez7-D$I8#2urvG+Ij4i-yxu!(PY26vddG6 zV-z)0{5bi0MeS_?etGj+`YQtQbI`E{MHla`AfhDHK41>Xu-;`)+3Bo2YMLJ}oF%sF zJ(!|?dmN%r!1ao5MBvr8aTD&|>9RrvlLhGR>P^An2#vDK?fG7aMwR}UVd>f`_>R8<>!=?=7%+JIk_kxJy7QTS$1Dl!xH@atun`O1~(L0 zedwF)nx+Iv6Pk&-P{g<2K@}TW<{GMFUC0zaw;Z#~n_!dK-*B(Fu*$xudqg6WBp;dWS{(P7VR_uE3pYCEoGQRO z8=|`!)(l@I+v|-#2)%V>7Lns4F%C9zB0_O%+v{U>+=#3(Y4jA1ii#1wRqO!v7Pl!v z)mO;7%v-=+os{22)tvZ22J0Po+)2q~673;oBkvE-mCA7wMXLiAv|^6$ZNO%YGo>5u zQmrX^Z#BQ3V}nPNSTpL=c#)jdaeE7zKw6;1(AXGl?PA4vs`+9Ohx21N&0}<0nL;yt z5OYt%U;-1SI*a~27zmn{RR6bmXFh+=ZuBJx$Q@u6Ev>XSTS)j0Ey)XL5tjO@kkCDJ zuL8o?5~|eia#s^*jBOHr#h@?IqE*|WysQl-K+jgM@U>IOm#ERG(pwgy0wBos=cL5;hQU~e^ zG$WMgHkwiy*@i@UVI^V*?}FNP*+m-A*(Grb z)6Fg3>KVFI&?L3uxHt)|?;YPaK}i;D>Ea$&g4Gwf!Polb8dd63Me;PCIp~bW z=t^)Brk%El5tpOCPe*QsRQEPgKnM6Y0U0#={O zii$&oqW2d{LxG&y#XQdyxC$YN+S!Sc5f^RZGtQ>0scE5hlO0QM)Mt!L6;cCIyKfF@ z+U*|fEUKRHPV1_UWK{;uIkC~eUg3dqbWnRNu9{*kTU;TNz5{0Wvl?hM6FG+H0xHI} zg7)L{uD_6>!DUf2P zwxKwsv^=_ag}Urz_=wN+%1*wz-&2d%&|fVU5wq3bx1r+UY1An#2XqPMevla4pgHC> zfW@heU+fp!wHhR6 z4mHEe8vw9aZ#CGeXz5w_{4@IT4~iK8v5MCWDDrd}geF=GqR!~|Ped-f{Gv`Tkg;72 z)Dc1KutgNs*;9=#|AXA}?_dv?i)LzSYW~@5hTZDgiNT)>4AxMb?4YGE{tpq`%0!Y_ zT@`Vc1yVQ+*ZjEhidtD#7cbp}02qy2s)1dF22~CP{R*h7F+jBpc3#25ME5Mw*l*K) za`=@;R`b18cqwU`!QrL{(?RhU+x$n3!&=w6;b!fTU*z0zX6qo7u6HQFnM&$g3)LU=DtpOXR6y0z&Ar)e;(Z@f2CCv*4P zFY>`841)DTbC#agysjoB$V=h+zH&l$haCGrrMURoR#De+gn5N62WBXj&e`BD`HT0@ z&r{j$b+RRc8e&S-2uc5&)2)K|Q_3v)qTvscoU!Y}l?H!Hup*^42kiuoQd=wY!i*Ca z`#)lzcEw7?2XJO^n9IA#$;o*;vj4A|5Xk=wUjwM+lNKpJ^ndhfvb`?|CT&|OXw=2a z&A-rvI)jW_2#soDdPycRULd<*!hf0$C*xS6^iGw&dN#UWhJf8gCw`z`bRHKf20E%= zM5nb}1ABP`_3G{tJEn9tXdBD%Ua?^7DjERrND1H{au4C&UoVv%$WFY( z=D*@uDN6|_BDg&c74-XLW+*sQXJjv06Usi7FM$P6PkJtZi!F*@t3DqZ6Y{?NhcT5} zPBE5dra}w~UN-DNNABdPbM@q!#7A;JM2y_tpN(HtX*ZOub_Jo1Cw21v53M<5eriuf zLB~Ij=-)tn@Ph!g3pWhL{_Y_c^2k()gQZ7Xh{U0L%{981>DCn!WL&{280ZGX-y*%3 z1llt4VYf)D0Rj|m%sqB?6lvAO6o_+F@z(?N*qRgtOLpI6H4Wod8f|ry%|zuUpMoFv zAFfu5Ngs2lH&8(3Jk55p5p%9TJk(!b6v98iy?H;oru=^Sr&=9u7;_ocyzt@Biulow z`b6jLLq^!%+>P8mmbzy<)Orns6BTeAa${)SLDF!OW33Rgkk*vH0680b8f#r z_qCz@_VZY;&5efO7ZbF(o3?yki@jm)O`MvxgV3+InegW5W!$&l$-c~=?@i5I9xYlX zd{0<+y7nL3>zZ)@5NE*%<>VMWc^}uS5)x}c0aw!5K6ZFq+i}lp*XUbhz8{=%kATAC z$-6(Al!q8%PvIZ@pCF7BK!H+ip$*WOQ6O=p2cx9hS4ioa5~H)dUMt2JWDGZ5AnKBrhqE_f$|^>vv5ba3Ge^fQ?GQepj;NF9-|<(1*RAibPjLH(B7T=y zPZA#whhUMbXK>r*y&MqVNb)u4B@Cat1gfE!Cd>%rrtAlJ;*RSvVX%LTyHTU zlQ;A~T!!=})RxQt@qWeA(-XDkTX$Zho>_GX+T;YMQ*lkq7{`YrLRxS&wwTu(JJt@+ zl~ylTucRc%N|4hIs9tDuulk4|u-O=rDx~P)FhWKz=n&n@-d__~(;9B9+eNw$Z97&T zzw}BTV2lMD*C6NA>~a$96io)cVywjcT+(Skf0M2r<0E2`c2==byIJtZ3+_c*L)y;r z{=C^t4?ZRBrO}Bm%bM{RogI>stpA%sq3f4$K3rVcGjlZ&%mNmzatSKkAHT3;YZ;mU zfbfzY^EToiyx4~J`#??FT})hmpS$NMZyCA zE^Ev~bOP+|Cg;HWd8MFRi}I$pf4?lq;HtskCqwbFM8oabkn<w9e=4QLoGHQL! z@{lu&h3rK>J!Cjer|p-z>o}d3&md2--H2>;Of4B^_g~#iC+#ZU{o4)mPLHgZ&FJ0Z2r}Z0u)%%n#_JVZ;yi>ZMb*io@W%jpOwKV7X z1vfuaYBPnaxTh!2Ew|(-8rHgP3ixd19A=vAMJ{##N=hl+9^oYe03abAsbQj z^8l?h(gM@(AzkmWF>2Y17U!1rwDe}OmXnsXpf(zdNW{q8>zuR!SB!}_kb3IIQY%oiND{H ztvsc=)`vGIjf-|TK-+-b7Bed8y@T1?`uMT(B@R@~N*5O;EHJ+SqMc#AOhk_cEf{G6 zW^uAkPb0&B&sf#tQjy?ef-RFIyXwAE{;~g1;;Sn`S`Lk5asALAd6By-T}T() z*K8|PCg%IzWqPaQwwgP8(^)gWhHUs(PF1!AlPPaz4o`B%XGo%sujU^;Tb|77(bT7E zIY}Mmm&&kHSeFXgsxo=cP->cQc|8`Ks}<+s#|3{J^q}X>;&h0y?+l(+?$Uk6&X38U;#nA z_8Cwyr`Fae_(YsK@e7KWF|JCavq6#L4t5rr)g5hIXWuW%G96$}))6WaD>O6|nlUSllJ?dFt!u>@3W+Ji!zDXi6B<)_j!3oN z57AnI6&fouc4}nLY>S~UI92-8cPp6M;VLerog1!tPvKcbZS5L!DyJDB2E9P<$`V0PFuDuQBhRSvH@raNHqGX-C$r6q9-W>6#Sc;> z>-q+`eB!P2h`xQLIhyVoRier$8g82Nz!7|v`n3=`dlfs`wAqEjrBJvk^PBvtY8b9!@^)&A#${g| zky(l}H71*N;AReMs{J^(53rIwu-GAg>A@O_Y0$rWN5P>UAjG=;ha^qfwzmjj>)lw0(<4{!n z_HOOxxAzV&>95b`jAOz`0U=g$m5<>}cR#tmY&mZA^Sgf(PJ1G+q|?CYzl2&&_%p|Y zFoiWUU1zn7wnx(P+S)Qmw9?3Evh)WbL&e3yXI627f;YM?wsa5g&X3vjJxf%_9Fhnhr9ck5P=9z*iZ$SsR{Uoe^U|QXH zond51&8bWKYkMoeaLZwA z8h?nud|z7sApEWniFWirfjgL~UXiVsVi|iJ`@IC($9;Xbc~jhx?xE7Yxy)B5FvHWH#=^)to+UTclZLL*nKYw)JMW} zeaQQmFyJik9dAOe3gNLzVRi;ebiz_T%FZ>jP@n@u?yGDVFrO<*E)3c>(Luk!Nus?eHJ#M8 zmOU#mnQQfhyX^?5PI*dZ6Z22ecR8hqyqIg(D#oqxFp&j2qnAH#O+@;b*Gd!JRNUxZ5V@xap) z8LpwN(31132%zrpaNdm$8aWA>Pw{6U{7=B|gK#-q+#XaxybdaxU;=Bro7FGI)E!Kac5f(EF8jTHdWO6 z!c6f_AZEl?B0q#$;$VOasH>$BJF%UET~$oLTsN`{S(g>*K6EDDDyy__pw|1~%i_7n zOV(ZI2;qKJJmLg#Uy02*wIBjDyVfORuMuP=1Fl%5zwylB}_HvS-g-R7_n#q)SR!ZBXoakb*@-`|<}pKW`&@xH#fR>@l(P^DC+CEtGSnI-V%VI=% zLPBDT2EG=MEZ2MOIk~A+p~j!a?7lv>J}16RPqb{s7&KIvYbl=@W`S1ldsd7M_bBvpuJP$xbgCKVO6u}mEY6DRia-g z`?&C(jT1)AI7aj<`N66HDf73_D}ylkLoYjT9}pm1y6o^)OK9y_2klsbOaq&x(iGlHc9Ymtx%{ap!9TbVN9qEyM`c&$vh< zlYGB9K3aGG;qnhy{l5v3y(pkO1?=b9U28i0xY+1D_`Iw0B#~@gfLi+m!I!iK6QdCf z2_l~D4-a~j?(|p-K;4bdF^;3952#NEIwC5}`t6#TfJmx7Pt-1vc?x+(Ztm^R?wup0 ztGtM!I4Vg@8kG5$RJW$j$sc~1{{+(%@-A>RbR& zTlqjNx(4#7%iVSnJ=*Amo)kMV`M@#1sFixJ*YkNIbmDH&+T(*BMf>z}Kjg)3)IVAd zL&>AIc57WZweKgZ&UXq1dIodh6Q;+Uv1$2He+s6k`fW^x8MxOhxbY#yC+n)G=a#2% z;CZKv>4mF)xP11p)8;ZnK4D{XK0wSlS~jYlIRc<7qYn4iVtEF4&1{ugMS{b)7KfuD zKM91XPxi~J!tFaB+(rVmG~Y%f)hGyE;1*)yR|M`N5LhnwL$BacX|y{ExcAQk)tVyk zS#JOAH7Gmy`#>|O0^T%yIdiS2ICgjIw3R*IXjuBiP1kZyXf!8Z6#UAakU$4vyj=D7N7FWB?aBP{Ga19k&=h}63AJ1v(R#pon_$4KQX3cD;daFT0%9TG zzXTl@+H=b}_~?Q4_7~&L(E|+Tcigh`IEwI7c$_i)hxz==#aaFUmEfm-pw+cYR&bLv zR5%cj+D!}Apr zNyFi|`t9lTS#kJf%TYbrr}a3LmDY5LfjjRS`SS1l3J07oa@EgBMvvl?Xg@DLkNG~v+Q z?`z;zqy#V%j>I!e@=E;m>;C@Q=y<7R9S}wpfiRyMDF z=mfuI<5QD^@r3$CmKcz2eu}g`^;5R`jpT1b^WnxOLcKA1=hb|l4OiVT**92+*g|F1#rlT6H;g4Oe%j7ji zXip_QO5ZsG)HHdEtx3GS*AJYuoarYT^Xn4050?RL4KiM&FkSzUo^}$s`O>!$T8bSZQ48;EZ|n3u z$(BJpIi;C4i@0yLzKD1V@+>xCD0gL77%92jTq?anhYs_#zS@l68FC$jEaF+x}Zv*%HsL< z-sgSfk7!$}+^#B^dw&&)`Hu||EK}}3&=bcAkhM$ z&!F>=uju@ri0?g-mPG)YYVrZx_U;74`R_~)csqs7eC^gRVtmlb#QcP4z)L4VC27+q ze%|6c)%&yKYI#kbSGro~2m$U-Au1+*Z>%|=j_>3nn=covdjV!If7eDKj$QtGEp?Dn zl-Jr}ta@P|=w=bv+S-2MP%n|ZrP9Y;Q1g>94Tje`k=qUzmP92+c_q3^ON*Fp=vqV6 zanq%9D>im$Oa)ytmBYu7w71Tjekly*Ftlr+O@7&`oA#s9qUTn^+%g(Ki>9+dl6RHp z_vv)`0tOKi5#=RPk<^rSs?}Tcew9ivWe!s2(=)4;R~D|xHWiB6Q6BsabksM&b$3qL zD;O(co?l%o>PuRGn`W{e#P^ADaB`Ma?P<(pQQp;lc9;4?w@miN)9i0v2Y5?a+TXNS zSVpb`Wa*QvN~UZ{Z#q?QY7Ofz$SEjg`p>}qFJ;imVvV|KOPoNY%$ zu(nE)xZgXje?z{ELDF(EWej|hWdE{89`FbV{;6CQd@kqB+cLFFW+kkyB0w<7a{E23 zH%||!w&{nP0-34Ec#Ef=`@4rfe_V=Gf;%5SuI&Do(p?kx%~tC8d!CK*h(xccW3RFrtrE-8qgBrI9wxnYXyj4`KRu2F1ZZ(pohn8bv5OZ;S=?*Zs(d*NVz zx#xq3&Zq3?=9SxRb#?%VYm92IgX3HoV)~52r?l^T-D97)#zTgaG@SHS@3s zD-tLDnM(5Hux|X=X>I6UwGEN;`;m3dhI9;1UF}aOu4o0>djh|&WaY2qdH=is zsxjiY%wa+H{71=2y&6jX_@`zQ4TXu{u3TC)YkVfd)kZArCp;#uzmi+Ay^?n=#mx>8 zjd77^OMUyDQT&LCFljnRt*L0|*IPmE>Nn?MN%(NENQ}A;vtF&L%|gAD4m+|*iDMyx54IAzRqT#c5L~2iaW81~?n++b&C9S)%_{HJ;_3`|?wnqu zx!@c`Kv{yx@)0gV+7rC&56gSCHR1D@b~IHRCzYzgqmGp$!j*r0-rCei6F#Hp~GTIiI z7H>4Ui2g`AmkglCHT*UWF=6%LkoB&_B z1!!CJC6_1gth_s)nxdX#u>T!U+wgRJxPYrzp{t`&I0UPTR#e9m;~@T!WAXm|gr+;1 z${s{Q<&e>=Ly_s?~(t zVXif4O^MwGc|MSi*CQ4M$r*lAiz!Wx{Nd|5WO0Bn1$*d8-Tl(j6NHch2A|rqMnMSG4N!ctm z%3)w&eA$SXrMl5s@DyDrosRM%8xol;%=6dMC^yDCzs-HM($_xaIK6 z4ikmZUSiewT)6)~OubcDm0`CwD&38AgM@T=8(#i=N#_v1l<-@iiaEujO84;E3t)y+M8^qjz8$V(N81oc=Fj&2_?i(wl2 z+|2xWOX-6@!w`Kg|HWaBZ2yr-3%dPB+*8ect~w?Pab1SjX~X7k+tREAif2hy7?C)4 zSd*Xm*b}RSByOMp0fQf2PDnfynuk9XH(wj4UCh%;B|c+TtC`b1rLD0W4x_-RSCyr_ z_#GP{mE%hPmYHxOWDmhS`qUPR1g`Z*2%&cH88! zsxvy^XDth@*PFeJ?VWcJ2{Mmp54JGM5CT=ipl2YsqEb2nuc9XczY5@QD*qM zJ;37N1+=E~0OX?ZqKeAtSAn?lJr=bio$jgL>3P}P?D}IORTsW*mmIuMo!!DGGS?Xv ziwY&Alk3Bo2tb~bq5Q*@gl>H3GXl(lwDaff{v8nd_NaMTzhsi7j%|URmC9V;0Tx+h z@sMXaCPpdUPGzyI?vi<6E0R!7<0l^^%lKFLn?dBV1gFd!Jr=tCS2WMmNrK7Upi0`% zOotyBQ+ez(_l7)|I@PSnQ`S?5iD+ATk$%ArL}xwv9Zw8&qeub1^7uoSBx=!D=8fWp zCY9Ts*;@r$s?m?@m#D64TNyM@&H-sVUS!|Udrxf38bWRkh|`Sdi7VMKy264R2=308 z1N@2T+GmOBB4C;uSAF;FyK7ArcJ0L0&^q*WlqWGL=Qu{Nekm9izpb44*4^a9hzRt& zb^%|mBLI->Flm>czrB0D>4f^%XZ90vT0E1Y;KAAy^k9Nl6Arq?V(`8|wCSlFzkdEI zK=^Svsr~!Kvdh~j@KjHnvR<4o2B=y9>ZWb$;FIrsN|>wf*fiac>uO0nt+ZwVnGui8 zv=v7Jtl>cp9CIK4In^b-GV+d>G1=Ql{9e!5PEj{b3sVjABhrwkx$*KAM136xy5D7t zVI55pV{^V;+x5-ouA(5r_cyBQxj@%n1HMV$r`r>|JHxk6dhA$&@F2x=#XwcW=Av&E z^jG9aCs`|PFK263lt|Q@5MeL+?^j98dh@$VsS6oA&p%pj$Q@$+k1<$Z{I_3$>c0|b z9K%LZI75Z#9plJR?ZceZ6`D!9c_e-MZ`K3REkd6+F|me@r9?4=f_yzCYV}*quvMPqPF^_y$?=Joxn9N+!} zR;WsrqwV!hWBh>_Zt%pQb+9b>3eT z8@Bmw-wcY980niRhhU*R)ZZRVg+{LZDqFPef4psSuqF$-+et`b*qF~K9nX)Z)DShU zXT%FHULPxaeH(c3jc+S|yZ^T*{&#ac$4dY<(~YY)`&U7>TqCFNI2-FX0VTv8+|3wQ z6>RGdY*dbtsHpD&?F}|cqAnhjvkrkJ@XdPx3z@PEqxZtuY)R^Q?Pf=C6WMIy&B;6Jhl3rXd$A2HAJH^;ly$H z&5>pB9;sbNfb2W~0S?IB^xzx*+`w3JT)b5K#3AH{TV25X^;zPbI{~G!RZ*>ILI%K~ zft}l{0`a=rjynauiHsZ4e9I4Z2BhGU!Z%b4p0UMW9z1_XdXVarPIb2w?91!7hKqm; zmk;U+4vFW8H4q9IUOuOPzN31%%60lPb0tI#5+H-t3dScog*1?+qpQq#WMpr**$wfLQ=CXc%@)MR-kz zsf?c?E~ETr{fiAQmQ8z<9&W+_-M}^1^%Rdf50Q28cZx&?OJ5tmwg9S}wxwq9)w`!L zhyOd{Mjf4^SnWqPfFBO0oE{04@pZ_r-VHMX+73pt-98r^8@U#A}a7`OCs@R9DwGAK38aC(s? zME+ELVk%p|Ah%X0(p7UDH`@L|M@m+nkotpl+J1HTMc#&b99ZK^UyOdMc6jeEs#KFp zKAaa@)7n2#V`}oJvi3Rj7vkqBM+qXyhz8+Da#Aj;om!nmxwSd{l12CzOv(ww&rX|Bt(r#Oi{aR@#yeJ z(*=`jYHMq~PQN42*D9+(jz15JJmdeZOt5C|y&v6rV%D!eweR8LjVqvwc%5K~a>)IWctCewo;|Lp5aGg7waL5L%9HLucH&RGZbSB} zjGUynFDTMghd5Q$Z-PK$Na|YFREG#AO@>(|5(Xwp{7F?W|CRqMhp#lD=nr3%S(Xj^6uu)9KFkXu|W-X<^kREyW$>Cckjd4VYAVE`6V zu<9Qyx{{DiM=T9v{TC9I!2?KvN`0vR@2&(cO=D454}%ij8XJ;;4HE@o(Vl3~sT11N z7v>JRKsabruJezbs(XNl!*-4e)+zB`Q~cB|FgUuI6pp@Y>mMLN2LtaT&x&h$Uz3h( z4DHjw%@^fh<4BaD_SHsPc@IkYJoOV9@;bgpta8B*UTrzy*ZZyG^lz(KB`or*RwozQ zi^NN?f{i1dP)le&Ei?x02Nte_I(1)*9oky$HoJeAP=4H2W!tyzpmF)DpOu-!Kr_co zbD-*f6q8HG<&Heymg{WMlHf=$XO0rWV^aT~iliGU{{1<9H<<4HV?Wo2>8OiovCx3z z5U;GyDQ(Vg!4gZC5PNzW5rj#X z?Q$th*2K5Js&ks)EUr)5h$(>bsB(zK6zE^6z-AW>Bv*W!1XLZFZUE9d+fwrxQno%2 z4kN;bV99|F)*le!oEWoy*E+u#)i?(DXw`v6>4EYV?LYS$a2{X*$JToy z&)x$FfAeI0j$g{@TOqX+`(TJ8yf4XHbCBp*a(t%rb}$#?-9zSyH)wjpdqc~km^<%x zzsEmw?$i%Iy;^T^|IxdfSz*X})H{d?%5L47uL4q=F>WVkgI!5)p^yD?0BD1z8!+U= z830wkzgBP?;>^y^IoE_CrGBmWbVh_>eDh0PJzC*@g~yzugy_%&x@2+Uhd(OIjAK_= zqo2sZS}51#4xAZUOS`5EBUkKL6HMoWO*Z_5-rKc&ZCCKCJ8;q7#RGg- zBCyJf_wPNN3F|pZ)lw_muRa;fxuH47E=G=EU}t=2X!t6U&-+M%6A!ni0!sC|Bq}qI zw*aqNf_fxN84uh*ap|`0BQw|PW8cjAw`bTl2L)({q+eqYugtr~%Qw1o#vsBtb$Fdi zNe;{j!j*?9sjYw^^>!Twqf-8_{40sD^oBhb{=TbBb9-o5s^zGZU9s=2(tj_TA6xT9 zFEPy5`i+nbBUBqFJu81KeFFhu%(j)Jb>7p0^>6j`;%P+;4J2L%dC4|B)BO>s1nT3x zJIRL1PO88mdd+LasYQOg3nlmEYr4;l_-jbOTHa6L4L8cX=%6(aSb^pdu@=%;s!8`r zRfPAPKb~$1y3dKWDuvUpOS4$JqxS2{pfAv?x%WjEno&qZGI`LiOSJ;9Zl}U`;frpZ z<5@k24-VBxIJD(}TD=mBTIT0m7htMg2*wBXvo!8g2{c@UGnN|78GIvcdVM_lCvwm> zHKitm!D_O&Jzf9`7r0{pJ*Nzu0JtCd2(dH{L>O&mI`~tx`IaZ*?DyVdJC)U#+yhH9 z+g}s@ipvIYNf#d32yDO)M9sN;Aa&NGG2px&b8<#I+20Agm{o(KTaC6I-!k)Y|Iqxm z*|xV!sdK@e{d$9Fzwmn?abHW%_ohjRX6Wfk66}HtSiZWkRFzhqp;WY#>lx!0&u;^=L&nXdD9{S`y?KZ>{R~hZM z9TpbNJN1$F0Th5y1v7z5YjX`I0;iEk=dya)1<4yL;Cz9IwkcWu{Sv;Dzn%LQEr;+> zKWPr9uezGCc6OV3WVc52pTbgmzHhxPj2y7nP5K3Qz#y@n@`aO-Y0D#lWVENbo>;Hh zJSiYkmKi>pSDg|pp%iCU_ytI0&i#}X>Tv!am8(1dYWx!{z@re1e%x4`_+f>C&vkH3 zql=grK4sN3^v`mo)m&D44_`*@q2dD|EM{_^fIs4(-{Tt1^vm1p%YYsfnaw&WUVuDO zgv6&ypI@Jo4P(n4yWdC16X2+bzPKVsW8~oDIRnHhHQat^@&dpR42e9SOLFAb^f_vw zw1?exhT}g6n@20TL$3sX3s!a4S6LiC!Bh49q_K0?G(G8?rk7v+rH@*D%|0qa*ZME= z+O2SmBj@l32@G8U+`UEf?7f<4DZQ7!c_VwwZdcT@jUf#rOEbnsvXM+awprdbl)Gdk z?h|AfYi-Xvwzojv99Ka`Ef?k}J{pB3x3*78&FAs$yyYJo#a052E5p6qUJ(b#@0nglhVft$M@=m+@b^Ku0pf&04xh2j^#K%^=tNt58I+s z8O%Af|Bx|F=Vp|b&IaM$`_leCmvgibmTD&;B&CvbOeh}pdmw`0U{^jU5VifsQn6!upTTjPL7w@4)gNQ_`J=a$^8B6 zK6$Obb4<9Wao;WJHWs`o{lUO??G1ma=wcO<$}3eZKig;en<`bG_|u-pEL+EZ@gDYq zEs7N!vPzaCSM&HA5kl5gizFpYh&khWyhf>xf8g+tU25~*i={b4?s?dG)wWX2YPeR( zG;J37c|fL_dyv}}V)*euY_ck>)y}+?c&YFUOB@C-K@Y@osaH_`cEQfYjzzhfF_nAM z+!G$uZi@m@o&OZgA35f*{4ARv;AXB7k9~M9qtPJoFCnt%+zeYy|sANW!d?c^HF7K%C1S zFp_X4+?21_sN`%Rjw)ndnR;MonS}U^gE0~ zb7m^WJMbEG1AnCsQY1|S>g{>^x)qs=0;B%GZ%}vc0Ud=vRcpo;7n6R5+Z*N&g@nTA zA)Iyk9SVkibmosLHADXU^CWY858OX`d5${}1oeBLed~67n>60&F+UGTmi@K=yF)g2 zN**-j@A?hS=1M{fHMaO*c1MRM!PELc*Br7KWY8X9`nG6GztQ8VFs33j!F@R0`^&G` zyBDo)PHf?$x#)J-H?dG4WL=6rx10O?B6&M1GMOITnfE|GQuTTFIS@(6r$YKstL=~$ zRr?nLyFfon(bqMtp1DGjwzfi`|oQ(e2DJ}d; zkA$3#Ma$sSTEnp0MYfB2tNZAqcY>2671(^*J!(sfbx+QF>Y6TH&d}UvH0V(;aX4r> zEvPmv+@5dpnRzOL9Tj8V$+S-uRpfz2cQ}Ua@v0;^Y^=^^rm%4@2Jjh{12*Bf3vwBT z-;$&TmNo3eqY*enwB-Qt=vU>m)w^|^8uI^e$^XHiPJ#fvb>?0qA!n<@IsR?+ANhip z1mpO98@bm_IKyP)-1Z?MWrX6Mqfa2I^;{v?CzncaLE?nbzb66P2aJ7dh6=O8QW)k4uN)U{N0y3Nby#S)d zQ?_irySsagSgi>MZw@ zkEKl)yE;rXsU$)M`R4V2r~cos$G_!wb#~Tv!32Ojkl)Kq)d_Lve=Un!=VFa*XoqcnmN6AGt&w+F+^yhy{R=g82ZrwVi`CZZ2C~Up z34Cwn5TOvG`hbZLR-yV1_>c(B$BYiz$P6*VI({++xW1O;b?e|Bgx1EcBBYC7NvwvT zDRlf?{j|#QgV6&9qXPD~4exu@l&gkI!Nb3icM)q0lVC3A+l`*4H*0>}5PR0~CUG&p{W%SIzET zO&0{VoPM0~wXkr_3g04IgP!F-TvX+#H8wgQd_2rktEtn&SJoar%W<-uoEUU2$}gD~ z=@qu0E@{bA|Len&>Ka;y*YnX;g!CULPZAi!(MHEvVim=Q4`D29mpH!7F* zJE?oF9(81f_cT*iHtOMP2-Q>_T}Xzb$;^5(;yy(H1wT%hT?Y-5S)|&$ zv+;cY86oU|puOX8#*la?)mzj}=ZOoI8hNeBDw@_jT*xm-fdmZUJ(uk)Q@Ei<5xYxQ z2pWw8!NvCaRoE4`Nq1O#YKKaF=&Bp6-CAq9Qo2B5^C51_C+lmvb5`^9z`?l$9ZPkk ztgkcKG4IuKkm}_8aF!G`;_OY|^Ksd@5huDMB8YN-YW_5E@BDh6fIKL>#eZ0!+Dj*n z&QbC=fhBqHO5zJINOLCYOYvD){+Fe#&$e@aV4~zbEq-dnvE(iGQsy0ix%ub@C}%95 zlnU)?oZ>hJ3RH7i28#C{wb4WbatCm(i>pSnnS(B6v3!5QVK)l{(X)nk2)_X1^TP`) zsNt<|wWd#7wfF(*>#4=uc4LtY7Ax}c>jN0@AG zmC8utG|$O$^;7u~lJ|>-;ge%Z=X7d({xYsZxDY0u6L(M5%G+q=`J}ZnV*vccaBeA$ zKgO~8D2o;Xym3)kQR{1KSyFw7H|P~CaNy>#Tndo-i5w;jpEJe$q2Z8m=|BG*_PA-{ zF4A%c_D3`fVu_6dp&7slw`CI)HsA5{1}xjR&5SWLbF`%agb$F96Ln)K*Ivo8b% zS~ikEyt#5eNK{Uici(J0hg6nVRGta`)Mao3-^z*+j~4MFup)=>yVe9ipxGKYcEIJf zunnuSGCt?qlXIBSvu{TcaSz(0PkllS+J||(fw0(!GomdWpPph>XBGUtMZmh@zZ^e0 z4~|89%mAHz1B%B z+OEG9E$g@soknlyV7J1*ep5I_x@_akZeE3T3pqVdnjTwqQCz!4K0SzzOY=^ zOx{~1SbRRJe8tK*J4u{lwpHL|9Pe`prR-U~9}@8K+iz6vL_P??yp}|AWm^@UA2i@q&nPOj?ANK&_@|iah z{g-~%Q#ccZ{?_&CttoxLL!4Dx%h09f=Jn}}H}MbYKm%3)0u1_%h>dY;WY>32-)BA( zu1h@Ez%b$M`JW(%`ML6U;++QP@EbN>tMl=TO{n}yMhOkQpy~DqWEn#5o6tJK)L&%w zy?)!T625dy)#LcxR&z%LSWPLd{sowkTK}3t*7gjU!Y5MD%}-JS##@Qw*qaz_AfZE2 z9ijzD3$Hri|1zaHb|U3sq`5y%0tl_&{0^*e)U`%FSG%$bEueV5VG;~qo<+Goq@t5h~(uv&C1FShLLlAWe zD(w!`=}D~q*#ViWa4MpgC%jv+N1IM7SK8HeAgTzJ7(yM}#=`i>^brm@leR-KmnS*p z01z@&gBr8sWRg7KRC6HyX6sw(b z=g)lwQQV$+P`h;NPDNO)!9>(>sKoz7^X$9h=V;qLHpX$sRCMxQUV2SDePu@?ZepQ?Ke z;O#0xB6g057wt#K#h4CoglHd{GC;upCw|fC37}fV7h8St&)of0Jy%tfd0DCsA>TIk zj1nSG5NOc7 zV%O@_`D@?@;|ki?yx+N{?=sZS7jq~nG5A0;(9ZnruTes4KdL6Sum5 z1LPhj1l*GUbq3mlfpEB(Lxaj~1jX_Js(BeEcbn@v;f7MThd1_y5OD+u8z8JNuFKIU zy1(Cw8IU3!nD=MfE+>;aZ)`N0Wx~b_k(MdZj5kVuGb}?nyTZF)Zm9`#w#@}q*?@#MZm-rdVRbXw4X`{|we<{w?x}TBsUR zAan)g?K#7b7W{<-GLs_W3f4%z3TJV)MLI00@~+D`aJCk&M|I9OmMyi2)bc=w7b4=3 zdvFDM+Bi6hW`ZEDne<2F&(f_W9p{|^eI$5wfL^ehnjOiI-W|#OGey&lktpmFQ~x&^ z_amfUNqzp#b3 zUX#B61MLI5A`B$j#;p~mENjoVZvrT~i->ahS#{&Kk9kJuoKwS>N7sK6q(Y02|2a3l zSL1VYyf>9VfS571nZGq_y@Zv3tYUr>v0LQ_e%f`!PzAvs2u7jeGy}nN?<2L2O`zZ- zR12c0nud(AS#v`rGc1C<+-4fFg>d}yr$$3k+mPT>0Q+j+>8GB{_4AFcSxF3GGaytc zhRN5cT*8B9WDB+%>^(y+cxzYGGJZ;!Lqeojz|4;Lx_t%SBy`;F$5P|HRN6U#!+xA< z7(*4d0lho$G;BLLWIi;X{~nl$NX|0xnf@)ye%U5L03da;4s=d zpF4Bl@yZ{8i~>b>drzK9@bYN)9b{*TKp;b6ccc_5I;P)K>8lX5t^5H;julU8ennEk z+m8veUgau*Ud0(LyNuMp*=Shjy%}5D(;`FEk?qE?9Q8q4^+NTmzVa;i=8Aom;A=@3 zAqm+SGNn}{E-rzS&{P1r9~|7RtZgkrSu_q=b@&OR_vX_8G}T)(k<$PBU}uVTIN$*R zsv}qO*(WBkib)$UT{#CNN_$}g-0rRaAmS3hv7g5S?+xHUvw6uN8`5vfPG;=wA-3!H z2v5sk&M|6`3Y#Vnu`S`+(M7LV%@c5 z)V4JKyiyqO#I+7tl+@gn)S!@i5CHmGU%AVlHKXYY&HVR`<~}=v@97HiuYF~|jyM`> z{1N)~!z>^%Lv8^n$$^JAi7$nFWdDf~CWr9umqlfT{imbE12nKsaVdLXGe;4_wUS1* zp^`Cwz8u+b*e$jIwbNCQa{2CT3Q3Zv~6m%>w4?UMs6u$*OrkslZyK`pvodG zwP5+L^WieSwkAq$m>Q8sl|u7PQy^Vhp(jO3OCJWqz@t6vJJQ{WUrRbc_N3=LMK@YQ zu@wO+u|#MO)TL2JYMA0Kqej^&Jq^Ef|JnJA{LnhY313&czF4oz5M%Q8Mke{??1ESUmP;nYT;Z5 z(Gi(kN`0K0^ zd))Sf)!~67%F}k@-D4T`JQEL=*fKpf)G-Ea_rMNuHd+23+~<KO&NeKF114LNq2yF+hKLxjdL_YOGC#rf4kNxHz5}2JmnGS=?r93~U??s5Q z5IGil8J_A6n^^@>kYjC{;d*M#MP>YBME$fl3IC^O_@7#-&2zr#n}eW)5VEH~6YbAE z`LI(dUDo|>>H~nel505+l|}DFGoChXvHf%RzOxH&(Y#9zYAZGYFT96}AO4IPAv#tY zeJ;?Q$^ko40yr>A)$hb1MSyrbtlIySUTz728dU;rqefD=ltj`fWg4rvZTJQ6JZqR- zoVB^^w0;XcM$>b-!W7}+nMl|M*IK=SX`{$Mvtc-4WU^fd216LDMO(3!*{({n45TD9 z{yR94QOi^QNoVVQ^Tqh6TAauS?cM#c9p@m?Kq$FZA@6%fTfkdQqGXOV79f;(E3POg zrLCxqo>ih9H-z1h{SyRE+2o-mY}!EtzVmTzYQ&Y*elO~40hSDroCj;*oaSAc80Y`0?&@3qsu9?+x4!pjp ziC7B%c@N6)A0HogDZdnqub#4ChMUE%lMq(Z^-s%H@>R_*HrQGoO1}Z9sd6h5_1t^g zw0E8A2iH?7bcX5dl(Lh{m}sa;Qe2Dk##29flFuP(@@h$MsF*i7>6sffb07sE_nl4n zmg?+Ha9Ay|kg zR1TB`#W|9r1=xLC=%=FG?Z&*NKpVqVvqYYE1F$%?%0d@XRMf8Gh%xZTz1r?A!vS20 zQr=kO$s-oU9~6>x|Jm^~w>tuwsfH61H3ULF>(yl_Z8p0}%H87o%}VP3B-fUxEs`kVjeF1?7gR?|isbP=^Mi5* zTpyT$GG^1`!&GpV*C*AxrphnKX^e>kzCCl)@l%d^(gNiP`zzyE8p(5`YD^9RFPiRX zd7PFITY$`JbEFJrGcvyV@mIaze=7PC)1NTq1xthY1WxSw}&k zN)tA0T=3SXj#e_z_kw4QQ;!dO8IoMLH#hu(Wav+%7q8iZ4810*z9SURhZH^`A^{i3 z@?i&~k=EacVcYL{Mv`D`nzJL?|HUJY+c{o#^aqQ5m2wU;XdTa25qU6chi4+_C})#? z>zrSROe2E%X_(Vxr90YnY5pu>su{8* z9@DKf`*8y#RE;MFx%yuKYEEZx^Say2rmuhKhWTdhkM4aflO+qblky2RSI8OVqv@hm zvZ@uUHe5;i68I+^4U24R5~XB_Zb^yD^6TtMqYhW6^Y=9@uI%F3hLTx8VPZ-OycO1g zFA?_If&6oXIw0OZ&)S6gUp5t0DcYs zV(@?XM{Sc=fz1%_yVFr7Sz{jpAereLTmhG+K|`GLkJ4NON^{JleNYV+XPi>alOJA~ z6QQV|^~55?dkr$bQhy4ed9fsb65dLdHRqlu!*W2aTQL?uRTh;_q};A5G7J*n%zQF& zvlk1cO%kW3Af9}RvUmtxPB4e&lJKEbu9J5|u!`?%_^D`dm@1;-8`n>OWGB&_U# z<5f+!dk${T^}@l!5^#;WfQRc6r}3kSrx#Ow2gQ= zM#3LnoKBEVFFY;=Zpq_Eg58&ooWW6tmg(g2^f7ZjSR6S|)xqXNysHDfKIKdXngye` zhT4WlN>PB)!1qASfWp&n-^o)8KUsc_db*y=coQ~zO!wDMiG3c5Rqpd}rJO)uZi&=k z3V2oVvUzUKvpl^Ol}Ug8b_Gz@zABA){HJsxsd2nN-w(-&h9}eA~r!I7MT9c=akKh6sm=)=Rg^dpZ^&JGSz?T#23?O zJl=5LxuBV+`+<^JwYLZc0Ua}kI8xjSzp05LQU_35)Ly!$Xy^7$z!4!2JM8_6#>j*t zvwL}YnQXlWN+&r%aYhO51CwtQLf<0AuS$UWbm6h_#|pfT2cYO`Zq{<`K%h2a$+(J) z`&*7Fw8zR~eEhUz>PMU)bt*THp(`6hC}~EoZRk?TKVB;{pm%kHdiV=>V)*dguIFc7 zE0|)z<>-K45X4r2kQ!GXt$W$4`570JM+7ws%HQtxjzcebj^#TI?ft0SG#!$h^<6BT zdlwh*Al>;A9wk1?NbUKDIsh^!818W{bUK^y7I_038m_V1?B@I*%$m9WC-C+4b4EV;WaLnjX@{Beo<%fw~A3GM2DcD6K)nZa-q+dAYJ{^NVo|l6WwB-Fdl`Nr>@w|DpK3kc*)7*k2mtz4c@T~f z%9R@_Wt`lk@{+W*EGdz!!>I7%9v8mwD_v&lv@tav=ll%24Hhg6-Xo5ya=^wo=-Mg> zJ&C1_^$%*;!yZB6=QAKxan4o3_0t&;$@zmFr&0oEjok0~qT3XeYw9+9r@cS}<)%eu zTgub}xe+VmYCRAGJsb)JWV-K1vtQ7g#MTB|tpGl%OT2a12amL|#C^-a|9!4|JwJY* zlS{I}H|?P_E9K0zg+cn%pdY5G6dwaqNH@e zgT+C!Wn%j@%T0hQ^bQzccCKOrpZj0y2<3Gu=(;nQ$lBf?fW*^@P+>IO5Ye0VB$(`m zifdnbVFQTP{-R<7gIuz?zb^Bc{K;;J{1b~1g5=R|WO6c1{AURixw|aQppfC`5Z#d6 zl7x{!E%63nddT>|FIkS+ei{x0PA2)H^_6-8(KL(?p6F7)DD3f3S#f9;DR$Iacb&ly zo+Ql}5K`3kLVYdYOHeu4xJs95HmbJvaVCZsBRi^-+wL#VE3!5h@z*6;p$M~{p=3j2 z)_in~{&zvoRzu0GbTZ0D5vi>3@X{ZtDThPNyZFhIeo&CB(hkaS>~y^I$nf9Y=nBQ!7juH4`}lrmM1a zw+^Ph@9`tL3z1O^s2!0{#7%!U82OqDfb8C{{~~q&;$8ovI+Xc$a4q`;d9(P@(g}oZ z5@u@4qo!j#Nr=(}4*d44-YnS^%CKC|M(VE-AO&}mw@!1-JeKlaSexu_gI}ujGp;V-37)=PYXTGM`lULKf+dde8G{r;X#;0URyTr zOkbj-Ja%6>N@C#au_ZKAN2f3buKc~@p?ssmBWIfwiw582ZL&mUg%PoUx~7%#4#-b` zU{^Nh>56iK;JYrHcX`Iz`^_eUHQg&Aqe%TYvVPj|`wp?7!OE9@kg03Ais5VlYi662 zCsQicffy*MN?ZqJ0CsIoVr6XdII557?dygsPnm-E%1GqUcgQIGy_|LC*|K~*oy)+A z6R`h+vez8D&_HAzG+Fca>j8Jz7Z$Z{Ub}A#O_K!t;Y=q`oE#Pwn$RwCFJc~T@MVl`WFxtTHi!Y(#^DzOte473YV5-L}( zoDp%yAU1p#B8}MK2rCEmW>2h!&dSIIH3WCCa5ysTn`QS9er&gl##{NX2X&z-;4;{J zV#jl&ra}O26i0vwg+XYu_5h@VDhyhK&g*M-m8MmRB=K(q_f(Q7DOxM1;2PsD_*w_7 z0IX*vPD24c^TCAj39HK639b0+v9}25wBODF0cGTuPkPOz^=;7N#;?b4j?@(Ody#b-&eetX)Yabx_`eK$@>!C0(FdG2tAlrf@kko0! zPhN>x-%3FdCJefVonDU_{~=Y1`)!ah0>s`b{kZqwdk-WV#mK)y5f@)aOUDRkOsnR} zJN@Wd9Y&((-ZFDjCvPXV2wl*Yw-|pRc#v^dFyGXxRhYqClYZ|lku1$S{F(|sIy-5j z>u60RR5dxh?HjO&wxJq-9`%UD5{hSEWImrlDJZE~D{x7)m$>a0F`qPemb80EGny-U z+NEn@n7_95n)Bgxs`j8l`k`n-;X-2(??ly`COv%Z!+qYOYy7@Tk@=%!Ui6{u`&VrU zGqlPE9U^3!MGw#SV_#A)>kJv%YK@}62074i7YLDW#BYe6JDUM_KhqMgZ7rua{j<4) z11rjComWVvgIPwYPStMPa#%mGxc_gJ0i-r0G?Z>@NV^}iLktfFlweU6`y2ybk4Pj1 zqwlSOMqF!Yz|(IG&}MdoiDb=0qP~7SM#(899q!Y9)D0}B*1dZs`l-hvA(mAs z(+Y>!ok-S8^=0TfZKr1 zM_xZQDkNM>a&ypXr+Ke>;_K%+xF##CA$qs~a z1;)QTXh*p|lb1c4PDAlc!?g-Un3QXBURN+Y(zzE*)L=P2xy*ITGy#}sy_P6D^xjqm_CWZ!?O^0)5AH@4uikHLre-8D( zbw5uH1g4FV*8}q7vX3*!#LXtG(OFrZd|%3{)^4`LI#`kjjQnWGng<0M3<02cC~X3u zBIK^Gugg20KTnICMcfF|_zk(Ri@2Q#b$yc)jHx}m7Cr0@e%ZCJxHU6G3z7AfU@R4b3nkuwr*tAAckhy*UK<`*!6N( zSmI2}5qF|u^vW_(dHS(yJu0ptv7Z$3-U67aCWi-I+YZUVig4F37=+*o4!H~`A`HR;h>|wT^ERQ_z)7=^tW<1 z%pTFcRy-dSLgtW|tpz@p6qRmMWLxcoov!DYRIZm3<`JZMmT& ze#KSsTDoa8U^Pp1o7qKu-EU^QD4koHlCp0V#`w~#NTl?5Bo#vwa~RI#_ROcU|KJ%u zrqomM^-9{UNo00pPrlJkNd)Pw;9NDQ=Nl5ZGPi~CYs@T~bN zThA4?+CFx1Jms{AkF1K>eQ3c?nbck-T=Dp%!=|v0nuxN&8gSwO7za>!CH{L^p^n`u z0HvAMd2L-R?9WZ??mi04y#QTI;7c<9YhjzvL6ldYR^~}MiY}P0K3zw&W@AqW=C0j; zYj;9Cv_nd2r)!`s!_n_~_&(Fst`8wrJ42do^x%xjTm*Q87 z6DpkXSC|1WC*}_Ra zsV{cu!>utt0e?|thW%~;^?aMm8BDJ=~a0ek=40?n#N8f(e!RvUi{ zEo^*G&|yeV-!!z~*?Zy-H-WjBZ#jo#b4^4WNLe2~UubO0T4Q^bJaHIk(R}rvNIFo# zeVN)(S<3a9R`k=aTFm<7q7?5|pf=_GCkrD4WHT2)G~q*qppaHj|61KLWWXu+q{Z~qncIk`q!vqR!id<2d;2^B_LE*krvac_;A5d`t?-Dj<9?U|#S7!b%90=D)!y+|e`7B(zT8L;GGIY3IP1 z;idsrAyf<|b~wKa!iU>OwJ5iMExno}#Tf~d>x?(}5l)NI?#UE(9ryGjT5oD#QYj?27_~}cy-lfq_x}0tchj!%$ltnNQUDzK6#0S^3>0t?Q`8f zp9CqmZJ%hUFP6Ah_Ko@$|IUtBS=XPrHk$S|`?*i)!!s>Vt$G9!v2GkE?5miJFTC-z zE8u?6L)ovieI{v8f%TPrv@87GEHy5eaTmE0)ZZVzZ3Ml2oUwSqd>;VqRY%F zCqf{R9yIAn+hEkM|0{dZNuF&mqm279z`cY`Y@nm6l#c38Gu`5CtTi+pd^#Wh-c+pNW2|oQ(o*HAF)-mpm`u47 z>jPovCK-%m%>w%v2*Hpub`TQD$I3VNdac6uSI?O(%F@&3J@x&O=UDZ|pob#KM0m!tp zKr;30<3V6vx}ZN7Pzxa`jfjeqNLx;%4dn@#J>2q$(%!3iEHWZxq=~>05Eys11iPNr zSuMI_hK(mF&$0#zi%9Kc%sSQ7&%lXDS`nTd{9Yni)q7LfgBIZ`IioTM7w4S#6X(9m z&XzI@*$B61K?Gf7+W}X-{}SQ#=>p7iavRp868CnpjCtbiJ%PFPoSyCxXYeglLaMG$ zZg@%)z5X{Xk7)%?4;-gniOY1rM`V3fvS>|XFUogV&YKrY(iTF~!l=Z?VR z`RPaT7~Dym9=`bGG-TC*o79K(rv(9n4>uU8Se;nvVz?EIiQwiY2 z#r~DX%aHjbC?-W&1WlbcIq%cNcl^vt=y6iF@~4o8EcX=X_t`rT9ucx|I^P|N?*S6p zx3`~6<4vGhh)C0{iVi&fEGRw}2ht8=brsB&Xt}rHz4yEg zu#bq&jHYnt=mq|$9=0zh>f45R`mm#Y>J`Ei2;A^vw*2!VH`(s?OyftL?w=D{Da1;O zi}9X$0)4gX-|%EH@n&%7qRBR(T0wSM*$5zNz z&4lns68hHfqF-Jr=YZV;>Futeqw3D3L+q(Ef7z5P{?&afg8j6BxM8ZSJb~tXIH_49 zh&l0||Ai`>D({rjm;E7`Ydz77OqV&Pbwj;F<80IH)|tV#)mmU{IPbpYraDLao)Np9Rrm>I)CFuRyAdOx>cf{5k9D41 ztmd

5r|zS_hcEOuUJ(7jsMRBM8{4-xbPL1cED-W1a7WP6Ca;o6^evdoxAzRRR7o zysso)CxMkf4O{89VwEA-*bRspZUH)CI_O(>zT)3*su;nzPet)W^)Y5$2sU4%AUzrK zl)p1vZOEoozqGTLGoI$Za3j;s0|^84il#eVGZsvx1wio_zBBHo4w!M>?1f(9Q}E@T zpnj|M9sc&B=fG&W71d_Qyf8tlxZ!(ktJQ^TqaDrtivnJUqNX^_E)(oa;SD^S0qVt` zhe*ZESf3CG+>K5WA~dsX%jxt$Bh~3M20hoz?mK|VL;D{CaOP5lA{8 z6P59;I_kDi5K4#vnW*2;YC`bkrqJT%cEKpLAl>sLvenALf%Y)=zLO>4uxFaf<4E&b zV$G5--QSOvoi+N*t`iZQK1|2^!?etK+`_DLZwY0d6UgbXMRzLmD-AblXVY4F7n!NG zI-~)k!v-TOwOt_R{SaW{tEDBsXX{UMK$q$Ob!}jA={-0YM|+0 za~$k(wk;(&ufXK}4QC`cmQB;{TEAR+;h*$GFc24A-;@6GUk`{atiKfCIjw_QDzOm) zh+qU*L0whqkumfhR1uMw?J=rJn>b196rV#NV8&X-MPUepZ2T1-zsL`Dim}i*2^pSg zu^&Zv$=^NJy9rHL>d*kdeucctVa5t<0xjM^VWyDYzgOG-;X%{M(egzJayd>y#XPLP zx(nhS$>a?ydv}327p{@=37n`v^}h>RvKtE4P{ib}9t55PIBwbvpknv8h2iB1Bhv=7 z{lh=-0I@R)a#HTG_r81M!1bA*4-75Wyi^}tmQO;@&=H}to)0<-{6&uA*l!AJ=kgi;|=YzkouZ!1xFl@BpWx~ z;m>|1_WQOGAi`0(-;n!{o9#LeSiTLG{+*BX8AWLew<#BzJ`N988ht`!D z_Wq1Z1c>y3$R#>*qZ!a&9=^!+k+|+lHv7yd@>q-;Tf^V4I-{?c{alhQ!s?Lw*A#H~ zh`te-`~@q1A2YoMa9EKc8w0gs4DYSf|Mu?y{cE&3W+$`uA(GU%^;77}3n(d%Ur_Mb zk|*xJZW0llfFk*7_A$v^D*g+MkTwI#;D$<Px7{95#utwDq)0Xvj>CZRZm>jgI_vXIzW}T`S!WtVvq_* z(|oB!*EBN9$c(xo;WVqR0U8HJ?H_8ofl{)8z>MdSAZ^cu4D*D(*3W@l-vs5S!ngem z&Z#SQEF>}?0FO6OeC8_9vDbe9A(RR0_4%^B^l^x;Y*va3avXo!%S6H!gqeVTt!X+F(8DnxacQm!-m@ch}fB1Fpb<3eoZh;j2v?VRANI(R${jH41s809ulQ?CrlFHtcpZRJ@e1$M#*6(F3)y zr1Cj7u#klx0rbI8ra0e#Fd}XU>ZqfcSUOV1j8BzH%%q{{~1p}PzDZw1`r;q zCnz(zSaM=@6aJHFvLsP1g6DfRRFFd30CYr}*#B8lE!r({Y$cC~P%cEyW@+cMd+ykb zcgAKh`DQaM4I=kY0L$+&ffEj}<_Mmo zM}=pUnJC^HT7H`w8GisW9CS5Ni4TCo|KF&^GnQNC*6x$4@^hf?7#=_7NOtD5=H`Cx zneh-lZPgp-cCNpA(GC()#k~LWW$M?VBao1;$s}7j(cS8(Sc08%6mKy4VyjW3w^j}t z(@3`I8n$uaqfstwTR_gUdw1K8G)RRRw>rRrR?D7xd8F2j+t;&Qu@&Gu#y+lH8fy zcsH3(jHPl#pe|S0pc$21XNxgFah~|O^lsu1$i@iYS^c9yP-!tmMU;nCM+-LlQ={j8E5C?Zu5A>8;~O^x#J%bQpWEy?F9IR(qxFs3Hqvf{C4kEg{`mHevWN?XuAnV{U|)Hb}P16IZ32(R_uKs^Z}lMW2B~!!y@=q z``=tDpAE=kUSUVcAN}`*g61o6Dz^{6*Ze`dzR$mh`M30|=lEJR{;j{4TWIMdk%7h> zuPlbGyw5?tH!NZChINzdqKu9spV{rcYRT zkhoivGp38>X$-y_#7*BP=?l0TNZ?zZ`R|rH%?;2Ew@x|Q^>^uP_f@R?PTbY63|-dE zz?-D5SFw09ZsXg|jW z)L&yLs>7ei?4IcWdEKEDQmg@>f~s$Evk0gOarfL$D5 zybmQ!Q*I7%40ZBw2jqFm?)^Dv$X-Dt0R=caWthmclH3a3aE=J`>Lf{sHDG*^{0e`- zxBE+C_BCAa>c{$ip7#fVu*1*0(k`jPx{JtL@`kAs5x&>z>*)on1;_U5*X9&&06UlT zy4@ueTVUe5Z-2Jgs{NF(x|ildas!x2Hu(mbf6WkS{Gqk4A&%RnE!$@=992c2W+@(X zvwCo@R!lse5^or~NG6pu=0d$dv6k_<@KL`LQ-1s63Q8v#xN8z&iPXEF8{$HUS0=;9 z7nUKkse*f4W%gCl!Q4Yntr{{rfiF-G)jl_4LE~Ebj#6LAATk12PEA3S>`X#I4WuKZ z;t*ayx1xJXY~p`D3T&ZP`pp|aql24CXY?jAT)tYY|q8};B%095J0gBgjHt+ktN$~M*GCE z{<2?iYZu8-a<&#q#M7~YI7R3r_8Fbp0*u~l*|K;^pVK|I!%ZK^tt3U6lpi76fK?5M zq-KPT*-Ki3^AeNrp21W-6d4z63nJ_U?(~@@1(w1in#%xxN;(Kf^t|6@19q8zSuK2^IHlnUd==PbNe1CFIXh zJnMyvNYSI3S0HHjkRST+>lT-O4~+nrmwHcX8fYmyWmCh^dHw5HJv9Dt90}?QG0gkL z_1?#2M!EW9p_zA7Nhxi4v^`c%P_#5~2o7WWS$Y|O4rLt%mjjo|B^^(~)p=0aet|G<`zL`n7VY~@}v%^B#f+4q6u62i|ul`gvYRMCM=7gOFn0QtziQeOEV?g$Dt04Kh0VNfeTvV>11_54=1 z+|(Yj^rr(BrCUnJb&?T!O?#%rBN zVIJm3p-(++wgE(`TCC$W2fgB?pe#}Jw6IWbLl5XRJ35a$U0sv{H?3yvZ+XXJ!U+f&P<0xSQPqlX7? z!*j03vb9tQpOoSidHqoEvxL(vP;i7o?f3+o;%F%|ezm4$CyI`&{z{5wUuD0q$t=m+ z7EHF{oDJ~}>WZv(-XCu#i8x-U!1D-H&Pc{2^!V#B8)|p{PgLa{%2!D$~IHPYj-%6-{Pao{ol{Z##%c6VPC*OZEas^ z0aw%~(kYEv)_&6jB+8$apFiQqcbmiyUS`@eN@kxFtKFbBP9?c(e2kYyDHF^2DatSZF;Q7(S?H1~o8j;J zV4)RKC?YYP8C!25*$^oBHHf%V6!LsZ8kTNcZr~9vb1bakl6WP0~j_GSng*azB}SfE+|Z6h@0_=!qi!3rcZQe`G=s9BAo}JmBvo+ zDt+E>>vEKjvRHKCgN<$iZyriGGc{vh-ItwLXVegnBfv}Q{ohwImElyDo1U82H0*0z z4#bDd7~B;@&c{f>=}QelBcd%F<9vICBr}PF#x2LIqP3K9L(XfR*Uy{pWc%OONJASF zg4YUJ5{DM>h6u*(gKOt&HwJo%S*4y?-#0UJ*dn0PU{*IPH-O4Q=0CsiP@Jh<qo2rUIztUw?U8;h-Wj5+ziV#;_krfxhN^09$Ycx099ViS-ZdMTF1G^;h3f?Fb39X z&4UwRZrlXk3Z*sRBGZkZwR;*S2x*JBj@omNj|aPe(rhM}_@;Kx4U0ZAA@lMhtkSps z7Er-I{RWWvto6N@$4_T;k<436R?@beqFtK9mw`ZV01-NZvhzoQqM0|XVUf4p(F@B% z|5@w=&5Kxuif~2B_UxzK(jy*KL^}3vOa!Xfdh{&g6`HAO&OsYF-#@f`CO4lS|1vDe zM5i8Rbv2m}eW-G+H0mMNqu5%cekd4}tFx!nR|Q|NWlmWTC&!5>?g$l4(C!!TRoOS?4~n=aDrxEFhw%RVdCU9b&N{W4Hd%ar(WvWK$?; z!;Nyvjpeg4_zQaaNyUiU%zN5f)gy1@cb4BDHsuGu#s-|7V74Zcp8f#73W&+BNt3Wn z!k_4TqA+UgcXd@G;Ih%7WslG1O(~;^v~Bb11E|ZeX-_BdlF|}9Vgu>!>!1`qvfHvf zpkSoh0es0evClo4_dDqIB2@nEYtDb|kvFJe?Yb8%$gf1Q#jZVAbckI#Yz-t*wyU58 zgC^YDMZ--{x@lpKaRgMhqOoA+x^xa{om#;J*bB^nbb;W9MT+|1XW*?mX+{z z9LUCo-^J_9{JKUi8do6moel;Sac@~KacZLco^at z7{Po$EOh%rY=Kqy*oElk%aO>b=g*iwcFNajKGu)CQ}>Ytt{USHt69?{j$_+;x((8? zHJ`R>`vY)-Tnv2xI+>|(@3YLxkE%~iSy73cBuA-ptdUvGCQ6(`&JxEWPS%Pkf;5Tf z$gw)_?L}wA$@uqS`Xi}-zt{b$s0jpo+Jg;@%>RES!y@~1VF5w@-`<ndFWgqYiJ+ibBW;7A8nSRD%& z;mCf3G$pa4e4HUEeS10b6kt`7_sF~zs%x1R6Y8nUGplAhKUbfECU;cC$V=xbo5+=i zZq0Q{F!#IS8|P(JdfeKdk2|XA$GQTp`5ld^e{XV5ko#NsXJlQ2yys9n7})cVj$vY& zxs!h>{c{?B%1ob+k=w9#nOC@mUTBl`qp|%9)<^y~oBgTPq2KH*&>Q^-0;%<&@!y~C zz4B;;jFUMIR*04#b}-N5ITh=$jlLH(#l}2&>Y^2rEfPn7p!8}&7jgX#yjpvQ;;GO% zDV1{vcS4_afmf?Hb-9=OE~N{VXZ7Nbfuqj<;)w7$Hg^H%_3OSprcX%2a#-{h%0LP}IGbt3< zxfg!lhoSiG$^>x0D)BF)1@ausvDK!G!8;q=>UPs@%O?6xggZ%eZPfr8J==a86hPUR z#^=D6g{LDyp{NZglTxEqMtjdiJ31RuP&^Y@rczjC1wm{ogsAjdQ1}h`!aWB;!s&<{ zm(~OVL5JFN+aOlMWELZ4N|wPXK5Y9stg8>xY#2H!{SwWcd_t~AVqL)Xdn0-0XL^X5 zBY?n^RoDNFo?Vs6c(pl`h-Vrkm?2AH|3VU0eo#_TY~7;4{iA7I{;*eR!E^dk zT7_225W!iQ#X+$yaIkLrquL-FI&b~3+n%)TSa<9kCK-5*v=Z{(gdLEc{J_3!sSyu??j_e=Kum z^2afM-~KfgUGlUVU1mXw*^-wD>a~Giyt(@ zvpkM74c!+^I}6l`)EOyLJdQl`8r)2@JC}fl#$;Hp`&XlU6QEZm24MgV_sO9W>??&2 zud?N_m&L{Mf@8Akx=nBq8sPH!upiMXZw9-|!?%~91{3A*rBfDO#?t(PtG7OOC3#Jl zIht|Xkv@acI=p_Z3jy0{DIhtVBlAO|T-|p+gaUmqXi`iq2yp50f`O^TW3*~_(lAi( z%fY+p51pH1Nk0w?{}4EpPE&(<7OUW%yeH?Kp<~GEW;w>s$-AHW`e6!-MfmB2!{kKb zi~&`+XJ zeKmW)0SDw=Q*K?(i3Col|E?ke(lGbTL70c^rZZKVsmW|6%35tt)b^}w)18{Z!r zB=&;>MJgbsVnjlNz-zl@Lq{`jXI_*jDoC97?q^@zp!`CNqtXNgq~K%FV{kp3{6hsT zSe9?1Q-q6j(RG8uR0w?i{;jJl|ZA@LEXMqfH$%6-P z{q}xK%q7tyG>c<8Af*TJR~-C8m#mzgHyMq%`H&gUq><-seRXVkUH>QU+&UUWouZe8 zOjlVJJlXPl<$_F!=oAhpZ1H9FzNqv7Z9X0PcDfdvz;wKv+6Mtm9M(oHP&NMpj(T50 zlUOHG=>qTJLaDZAUiW@fm2`iJ} z?D>P~-+&3>Ljj!Hir0T{GtTLyF(T(9{B5(YF$M+Pah`E*7;OFsDe}xs)ZmOaF8hqk zR$3Qo{#3GV@{8%lpN}-C#70J0D%+>uIiDi6+J|L^Dk?iCyQU8IRAouugf;!mj^4GI zaYlUhu>78)$laSRw<`B_Ujck&{~p*MG*T=93t@1P9(zL#k5nO;DMr>FU}Hy0+_+Y% zj|dAFWP7J_9>kZJXv~u#f`%0+R*cjreE8oS^e=-?GfkFjO@<^GDlQjueCZy&S$tyf zVpJxmEo}rdyCr^L$8Fxk(`l-ph+6$uPb?}G;+Viot z`F)H?^oHI(DqEF)+483GHW%^eJ(l$QJwQcNQ*GG2V98nO$a2e+lXBRGP2!aLRP?0y zeCItbamz6kuiMfpOly|P9OH1I{<8b@t=+2~m^HNP@`}Vi6_Oh5gj;Tgvz%)Th&S8C z+;b6;Ev~x*f+6}SNXpe_ty$xMMOZUmDJ3q_uPp{!zfJOMEr@TcZonmQUdB6}b1;cz zFYS`87unOm?*RsW6&hXeSH#^r;kp#EQ>!~;1nK6}%{GuD0<{|i{O22YfT&rn58@Gj ztpd9W@o+v1ZH;z|W5J2wX7SRTR&m9+8Z>UBp32gtQR8+9Cd1*Lm+wd`g*1je3VF-u zIkwojL%`nyjFUoR zvRMkk6m1Nfz>f(gonza_e7zwn;OT>oVm7pVCW_{<(em03r+eh?$8d!2fgS&u1@>^2 zTq5T`CF|iH%p{0Wg9qI}E$6}Xutqjs{uCBRtKNwgP)fakZ|1RmoOq8*8n&>QP=W6fm;O(Cn_n>sw{94>+%I&=!0lm27t5pbgA-v+ZF}g%8n_w~mUP&hrO2 zw@yDgoqjY+xgOv+L~t`8Fys7ZaO-Q&>w{m)c;U=I>BjGaZ)88PEh2ryA6qJrBTZ>?< z*~y^^eq8t;x>ct5Yr3v(s7aD(aC< zEB&&I{FiLGn(-#^2>|0*OdFM-*a@t552FBg`|cRJ?YB60))cEcs3=!`ONYM z4`UN0kkFWRe|K+_!XnzxphXkmWYgr!=wDYmXg!GYeqiA>QoRkmrzAu~oZq@R#1p*+ zOoPk^;x}_&h%KOVV3cwI;iK40a-AFkO(iw3FFrUX5?k^HidYg}L6Dpmfb<6eUlInb z={>;N3NN3`j5FomXPRJx&$$MWC`k4MDz4vSFI@mfh4K+XiNyDj)+5YqC@kJ-zmOf{ zwy9^gQ`Y>?P>}3b;#0!%nW4YEw{cg#qqqwD6ly+KFh|22O6` zvO&k8LvyovlZ9Y3w#aa+QNF5n2W$f~YnG6b_yaPFQub$Ji%}ZeFJ9#%Pn{}Hl3mFj zeTNFiSqY>Ljc3z01jk;X$Cv}T_9SH)#8U1@3nW32&2VdZr0@;^zf9#J`Ta|+gPt1&oO@3EJUnE zxsQVVu-AL#1}#6`XubmDng4rZqQDb;d%l)I4VA9bR!3{3f6zmc5Qn91(IFIUxFg;7OgAePkN9s%RAJnhw>Gt$&+r<||Rf$=}|etwntM=I`u`G~~V^608g&L>2nkeMTM--2jWDVeEyJu{j)cg8I-|AkYIi z!1;GsW(h(A^a70&T*Hd8@vxva^$#ln?|_m3-ilwVgfHmuTzp(xr9on))8@bIWpeu1 zMmlC13>{3<6Pz|X*O}G#TfaO6UjA;PSOfAD0!XA{r4i(t~MVU(N5@Q z@DCA0LLs_r)Ba%p6EmO=m|IB zo2`#chsmf8Z+GImZ5g#nzUh;uuQvy7oW0Ay?eTO4L4p&?Nh(WZNG0GgRvq7kS?|t% zr`;7psd0p*Lw^th4F)6Dy9O;$>An83vEs363>~e~7gA<`N|_KvgO}~_;DMsa$L-D7 z{LW_UdD23ASYpXxjGRMXbGUg`?jgpvM>Mta4F=PZo4)56;2~qw#!9FVkCLa>?~PUw z8Zh>=r$(Y0^m`E$IF%+pfEh|AzBgh=EMHr+wM$xJ(};`|UH*}!6H?1{TzcWF)!OB6 zNX=Ip;8NuAD=*zv)pRwd1B!~9>Eq{B3p|IV4Hlez_EdkQT6xC#B&FCT2!>ny5(< zC}2k`!bdmw@u_7}r}mbB|7239#&kc6-}e?&k=muT^ZaGpm)0*MaqJ3b+(*!lYz)32 z%}uu)!SLW4abwo8@D1Y4%*93RCg4>YX#eMPV*OtaRi+bBG|V(0h(Q+vFQn!N5^mZI zdOCCDwOx~Qr4S-tu<_#6VEm98?LfboI-L(o_y$d$&@Cyx+vAJWw_xe}yo^2$DPN&m z?`i2GZMpV=ALik2WVkk2!9nNBevkJG@bdr+Hy9duN~{kY*By)111A4V@cQL*!VA7c z7P2fk?4B3BSjs5vfR8(1vJLRBZnaVerN&Lkx5`t`I5H$9IF?LLg)r;TbyQGW$%9o@ z`+!{QJB(cI5<=DIz#X!3)?EEUmr%Y_A&XBh@0FZ{$Lit~Za!kOr?LnL^ zjo}j#LY)XDjGVL0kGqMTH~p=p6R@UIqv(?Qgq$z7Qy(#5Yrnwm)u=J-Hg&N6sUe;5 zgH5zM73tUx6$C3f01xOZ^K?}iQva|o)*Y4$uJb1QGA!P;eOYfblFg-ly^YSsIOEX> zfOkYJz)brS;wyE7ligZL@(#xmiRiHcQbOH`)b+V&FXFuBzAwi>Ywa_cAL;!;g;F#l z?*uN3AyXu)WM~(t&$-XvXp`hBxNGo)_OB{4Qclx3*N`|TE>gG75165`uQliNT<-%3 zYTlE|o?H%07jYhJnWxSr-X9T7{Z9G9^w?L|dcsD(9x0~PV2hx=PN8Rqp4dv1k@?d@ z#P8WEgl5w2VZ|f@){KaQR&i)zkk$?lmhJmyuS1yhfASrSIJRZJgjyLNQ~{$;6&n%K zT2e7XQAzsC1C4`91w)+CYwfHGHjRU(j0fVPK?iL-5iv|ov2#GoFFd|G1t7@7c;@*{ zn9jo@CoL>WeyuCzga47lT!H~|^A2pQqEv1oE46mlUoA-^6J*hb-TAPKPM<}}u9)?H zJzW!UrDup3j-L#>U7LZ5bn`Q=h2)cvbva|sfd@cwZ#UaQr6c{ z?3Krh^;;yZuS*i=YhL%H<9vb}9B{XkfrOG$A7~7>1+%QS=kTIDahfLBy89qa)u{=o z99ykL`v?Z`Q=334moeF0kl2{&=~KOjS{<*N_r>K$9UVkLsim7fI^AYJhYE^`M9H9wL{I68%&hB27!v9!a@bc)j9qpe@0ZYJop@ zomvm0qk^Xh_0d%j{^9oSBX<_hx$xTGZc9JQm4rR-uU?DEef`7D^JG^Wf5+VNR-6?< zFQ6g*6A1`+#&nVtA+P$U9dr>i#7Q6EeOF2NpxhSZBZ?hwiq1Y;##18r^u#gO#&Td* z^ted0fOugLKi|ELt^twHUq%9#@_bvdKiL931DA8#;{|(OIRg|MXw?v90pJT10(SGk zUs;k@a1nl~Y-aprkm5o56FO7_WAe5v6xHw$*!iDbf3}MU%djKj4#j&YS zyZ79>j~S9*0WAP-zHU?f8+;{hh<~%<7L}U)-nqjAe*#40-S#Ikly4z0X0W_%L!-%| zkXz6IGk!x46uyZ8gJhD)Fd=wx#@U}1^rk}=8c~V7O=RCHHU)>Gy_M{aa~zwGOj#V{ z(q6%$VK4D=isf0PY@4<36aF|uG=n;bDiDH-+qk_Ca1Cvp`aws=%9U);#Ckf)@PDs{ zWmXDSbe5@DNLL=)SJ6(s3w$~k9zwfyt+y>IVrfJ3ksv>ZT)Ai;bv-(+Zg1-nuyuMf zueO*eP!pRUO(>7?S#oJ$+0)m3^>JLGlrhQj ztpov)uw!0f0OU`ygeIwXxE=!5{?WGzh}5Pju_^!iP$)iupuumFFtDgfdbak)OHO*H zZmgX)ElNF(bc!Dv0X0`SYFpHN_;P?u8Cut0v%4#eRSd;DmS?yVPfr+aB4<_e!YDac zu+`{oJ;^D0yaDV9?wE^vlfQ@eCTD4;+mzYL!TA@2z{PvPs9E~iMZf*w9}yHf?HONe zDh9K}MY47W*tJ}~1t%8SUn&L<0$YZZPw#{&8Q$cPLiA6%@ey4R2iOTM6UtbJ6gk~8 z4>%}AfsH479gog_PvwN#j1jIuU1=G(CJH$JQ;37SA=<=YFHicJI%c_ zYqL3aFgZN+#Gz%eha>^Qxi>E=iPIKC)anE__gC)Fpjk6m3*%yZi*IL=U@4^l9r_F zmb(^$g0;J%dQ#|89;Ec`R6(*qm*3araS7?vnNb!#+mo>kT|5QWDWcYh@ygDsF4x=B z!cdRR2Vn!=m$Y)oW5dMkr=UA=1xotVl8?+va3dvJaujzS*Ap#g7l9<8*A*<$D{6TXS>W_l_T4caamh_9lBN*vdan zzZY-D5wjL5@6qh=DWSk*qx~ND80Xt>1Yp|$3bYW|;4se}+VNYVjDxBnH_6$*H_oR* zdAQ7~$ULZ^q~`=1@d`uKRL<-Ijx+B@ z^1)=U(odq|88K^ALsHeqCQW70ccPzqCJIta<^ROlPOn6BJ|)>1{R#K(o8aP_klb5V zq)dq=meAqmn4{4dANzWDwNi9Cr@+MV-QdAj(vafMb&6#MN%igZMHT#=+WIa;Y|D{X zWlH9K8S`yX5HlCna?X;^--nl#Hxe$Vgvx+lbi(Ypr`uu9;KADl`PY9*a(%`=!gXk2 z5Hg0yc+C2i(qI4IrU^W!T9?~7Ek8ELZb>NmMMzti4t$3mlMDfOyMeky2yb$BiRkuj zY{t@w-iLnG%~5XXfubdA+Ks<)x?@Q$OnW3v6;s68uTJe@zEGR@qZ|{t{Gvb=wNK@p zR)-PyP}ZDO3aTeu_yQTEF+_pYZ~_BNoU1NxgP!MZtc=x9@?ql(BxBU}#(U3Z|HIs# zrsEBOZi4`0F7qqDMDdI+t+!kg#MGOfh@s=&hokkS7t){efLl}&LMzL8VKg^X_H>On z*!PkDpR!(WbjD;%E3u10%`(98DjFvS1vH0xuGt#2d@c(Z&W;sfiG$v#v;D>y8kVtJ zop>!y`bk41N3j0Jb9FE$iV!;f)ZcvZ%M$gc1g~goFC8$^lQ-G zxlI4>^Mnry>RrQQ(iRnscylH650@}1Vspiqq6SlX%7j|sNm8W&6l2aZIm1E12wN4$6ZU}aHDZ5CiI|t)uI!? z*e`Yl{PWZ3$pkVWH zGCZT!Ri`=c-B^j+5UOAKHDXdmW|~H3{*w%!8)0;|`*w4#d+ErS^deb~C)p|bE1{cD z?JF^^uFT7o72N?__8Ga~x(Q}8#3u7{42c}OA8OHCAvi#g>z&$#?Biz!ccJJvFz zPSRG>kAzvLkX0P7&Fk@dets{Sp;gE3D|J3jb-vF&0;Ta-HBuDzTSBQuJ>Tdjf(48D zvsS4tM+Ac(LaLLGah8SYc9*7xK917O`p9gHn3kAv8yVR*iEt4rwF-_sy<6bQGbN6jidl7xc*oNRYAgm#m%`zrw%+I$a0Le+)ox&IH@@Q z|Jy(zfpogsRD((mL5|$+KRo`W%`hi`8ZM8_7i@Br`2x7xsX}q1%LGqLHA_N4^T)#e zSdPs{H$)=mR^tnG6Hc#6;-pG)O;b)sVrw~|g~Jinw*hh@(Fu_xCRtdCO5EY$L`KSsKS2y$zp_{CxNO+HmqnBB zVaYeYBXBa!PgitFP5rPVF@Y z;=*3v^XhIrVtaK&^?{DfD**Yp(YrIRH9AtY!B2^RsUWhWyf5fv?OtV~zE~1wx6M2z z&WN?kHskwLd~>ZbJpWDSUohnriF=OJdL9cP9@iX$eo0u4-#{|K^-&5Yl~q5DKb;fX zNL&49Tg6_k-13X*BVQ`llnAADD87v70s|N2(>>s+m5n5Q$M>%SKQ6Ta>ijCuoIE+34z$LH7yW{Ut(v3-J{q~) z!Ko0%vE>4Yk2}CVV`HerML9I$Nbtp_Su(u{L*O0HOa9lAU3cN$Rc5$-NgVUVi!T1zjI0SG@Ob01H zUsi25?nF~%l3?ZtiRDuI|MAo{BCt-wBrkp52p=qdY7Y=3*8&yut(ySW4wHdBxC`c9 z_^{uo6b25fRXhF7Z_oQa$YTg&zy5dg6-O7m=(NSRG1Ju?BeGhs=0)+aQO z2%0VQg7_^<_LkkW-DB~{R=D;E$+X&xBtNy?Ov|VEQ}F_g4&|;DEob~tBI6=&-piNg z0H@9!koT3;Zq|VYU?_v^8T~7V=3T*b?<-l|NK#w4FM&DVCw+JNqiOgkRZ&_POiv58 zbQ{w~K&N#J2nWy(eJUalnA&%@nV%rYArIbL%@9VfT>iMB_D25(^5kv+62RUm>5I$( z{}qh3`(^;V>h7lTA>0b4xWI-E?V}L zobBL#{{HzCHy<~r;(``lNQlbYp~W!y5$T$!%w7Mk)Kd4QV(7fPu{@U)Ot%*XKR8S{A!Gr8nXoyV47O(Yb6jd!`3kvHYC_i`>CJ+>3L?M450#rvg4@ z<9{KdAz6ds{kh}u)?}Wd$aoBSKPs_m1XJPthzA-Z;r<8@9vACqN==~>KdC@Q3k*49 zd(m6lpwOhZ2U!yMiwW`lM0nU1%tj`-AwU7NP?@t?k++nq5NO&PxhXHiQ9^~{5FZ3! z=#-VQ?lkhL2`UT_m5YCo+^(wja?sJ|K zs~4*airT)b+PMu|>d5uN@xq(XOZarDI@0TP)rCKB+Hwi^8Alxdapn9*&&{lko2YX@hu<`}t%&R4D?Z;^@OyN6

2dAm#Q5xDsV*ag^~|0q4s%Ix$|tsoQb~fXys9WyhIl zcB1{$n$YfDVqXI;3e$K#-di%e>ec1oOr@+Zxr1y9n369Lv_nKlH!vztyfMcR29UGe z;oAt!UA1_ri+s-i8e2eU#FW_|A?*Vv3;Kh%!~rD?6>Bw7kEpPMKqLe@*w zGXH50aLv0=YQCMvk+3$qgxt=KVE$k%u%tRMZtUqiEijp6lMJEfSHX?1a0MP{i8*CF zLjUVCK;oXetfsKh-)#Q-2iY>V5nAgH{A^2L2+-O>Q1qYGU<&aay;LedjIQ0&)EWaW zUNDm>`4geSd&Ou9rG>{4Og0y*1u{rGk#$15`s2_-t2s?-{DVR>3!9&OGTqhnb#3Wj zo1xAWzxFmk`$YCPT;eF#X<51{&d%0?H&mF~?E`WJ*p>GKdlUrRWdf@lmFH?~j`Z}n z@9s0`wo4t2mllX4WIq8HNmK!USW(0&;mMqI@++Bz=u+fW(qAG+pOKNwJ`tYdxxyBP z2s*T#2=yp2D?;B!S8)@#4G=P!#Dam;_lFJQp~9nhrGZaI=b7K-ejel$R?v71m=3&0 zMIn7b(}2OaxHJN86a7&%Q#Uvx_jSPaiv{^Thy0CYInazduW@EST!r(GfKei@gz~$I z%)GIVZr$P;C?BJo5H7(4^;tMRqdZX$9&WzN0+yME3Nci6LRv> z&L);m%t$n)QakaYzyCQbQ#;=L%%t&SHMElc8H~8I26&Br3)7?Iy%==(3M~=&zX+ld z4cM4%pQlkbQ4gi3(7uQpBIoii68y^U+ZTI_|oh2nQr0xYtqO}BN0Cf(VfW}eSYV$ zsCUAs=o8b-U`bwKBZnCO(#HjDN7Ooexzvm1Kk4#GdTo8S(HYj_ecCI$j$t1jeyC0m zf#AQuo?uK%1~*mXUr-SuJ3)8^NcCLif#?emX!#Js>OBEjzsME?6(${G+d)z-20NCs z-?@Gm@@-Yx7dxE>*N*J$GdqOkB-I6KD8~08ZPgYUAa|#((vmdn`*aN&%G!OBUw18? zfphqmpr%ZsF7%)S@gvNT8X_6Q80txRAH?K3O*2k3~6vA%O z(L@L{Kdi|j9v*gH%X6xCW-{usZ;~J-dGt+rOAgwXXp6HA9gjX(*0r1{#rFE`;wM#a z!T+kWU(3$&w7_UkZxnYjvR9t2IDP+___UaUAxgitot~eWQ zrq~6PPP~Qw_kyEb2Bd1gEqS_pir5|&8gEz<9^Y24#nPTt;A%kO3ef?K{ig6{NUPHB z$Plrwud7W#GVwIgWrHBbt#)r!K@qo;;qcah^r~F1R!jLjc1Rir6v%eqn}rR5YqtTI z+J6P;0C1_CCjccfgZuAY#1#NCdQc=71n`@3h8d9ojJ%+*&(*v9Fv#c7lkc{ZbM{+I z6$OAH6QivM>jZl}Jp#D~naos{w#g+IGw@l@R>S$a*0uzvCY zT#6EC?Pi`_j}*!zyJNJw=+sGxduv#VdC~X1>lz=ziW_{ADk(^wa(>9jPcIzQI7zac zmjW;QdH(R*s`Er1y^i;Zn2&|<#T0DjBA~6Iu~TvV=cXpP4&g@&fx^2aWj`4c;8&!5 zwN$f(UZ8#Re3@CvajiV{K^cUDb8)! zJgDY7ze7Y0jVn2@NgCwnohbZxkh2CD!kgc1TTj5}MJ*TtLcEkO0ekKm5aXv3kp0zL z+)lF#nNKExSolN}P_pT`KlXQ@)cd0A41iVii8n*|cr+U;o1@bIAF9qWEUT#9)-T=N zjf8ZEfOL0vcZYOHqkwdWbcyr}(%s$CE#2LD7JHv_zP*3=&r7^mYp(gk828}O!xDgl ziV0?H$xr=)IxZDp^af^G8IM5~=Bb48BK_>BKWN7`^CRDys3dxrFl%UX?#m-N-tlM} zT%G|M?>72+x&&vCVr&G7+enKcWB*M={EGf7*y4)l_BO6BE;(c}{baZbb~iu*)CHI{pa}*O5ia4VInBa`pnEjlfue<3R$%*3rfKrA!^RXsx7yV9`5_rV3S`5nIE`TG2D z3W$EKvp%jw-Uj~-^ueWSMRpna}=ArJSpk7jEiPtNPi&rGJhNzlk z$br(a@|D{_O`0g7Ji*Pip<|@#fFNV4(PTZqDZKE3l9}QSAk?MbMwMWb0gOm}CONN_Q}SCmSnuAyIgElQ5pgyvdMLyahO} z@kl*E)c!VI0(cD(nL0n#{b9C zIEIRWmQ)bp>fvv?N4fXSj2flUxqL-xAc&Hf2U3Mb&I3KXSTru_GJ>KbKgo07{K=QL z)#gYUJw`U@Zz1Fm0tgF4F$f>e1j@qy4WS))gx}~285ej2$~%wbKPS$X5}iROhV%9@ zRzkP={xrqB6+%J*BP6og)b1NaN1^Te45(nfbS&SjoL%%CM30<93z60z#JFv(A(=Zu#+zP}zgCc_#De@Wn=0$WrZ{8ndvVouP zhyEkAA+g=-!W80L4XVh;{0WIi4Uy{;({iV>4t=FnXEu*f7mP~;b6Ap4hs~_vjdRg@ z9<*$oy(uB>wSAU-G9dMpYVn^xC_)|g?uV!~+CohDt@Pp6PtKB_8?byEcx=L1Y{d?r@az7Ek~V!+^8BcL^YjU${z(*;zv1x*?C~TU7s;4*|Z~ z6h@tzUeFx1HqWKUR%WS5H|`BjJ0J(hfCTe>E?6C&d7f4?g1VcL@go?38Y*7eZ}Wqq z|C*zIX^ZJ)YR5>s04nVi%Y+N>{IQDWoCRf@@dL*bc3?P<0tN@i=i5PKt!&$Ttda&huEoknCq&xk*E9YjubZg#yc?-20*4{EVckDN-yJZYtQnuKG<7A> zsRaWaQmi@B3B)AGd*m*%`B(CHMqkk3H=C!ZBu^9sW&`BLp+}qN6OQp4n=Pp|o_Q@N zU(PeMsvx*~FNh6j2P0bS0xxe<`|h;Abe&hgXId7WqW=tOqx|2|N_2R8+5 z!(Strz|YFwVJ>|?43dAsf%y*Pc$hYLh4^ByRa7^6z1xd_wWcQdp707*5)NSOC;1!D zqWiXe949!7NYZLsy99vnN!7UXVig(ul^V&9vk4D$cQza1gACeHfnmzV_~76j3~Xsd zgH`bOVct=UvIp=&#CnbIn3TcSMRC<3&o=sxqS2};i^c#o!AhFdNg)tDCWlu)Nbev` z3nXei%--&Dmm`41hLgP%os8;G%m{)oqt=0Lv>v+CRJYxiw+?OqS1z?dSJm+wXY99s3}rL*;65JmpQ(n8FCqJ+0u{8eqQkZGhR@#(^ZjKn^vIF3{Ou?-o>6Rd>M%}vXcTnZnrGpjgVmL=eXw6|W zgKdfqsrq7;E$GJ=#M9R8lQIz4>sxRsaiDeKi!+hB`>b2`Ys`kLBA_g=Z0M==?n|NQT7!28D_9|-q(rO`8v|B4c%J(W*5jEnml z3Ie>KLG$^cFzu?^o9TuC##rr2>1%4vh2eVa)qKtfZ6x9$M!=@k*4|A`uWz}mGV24g z(TbkNJvp|3=Ev#47+VE3;>(AOu>h=caUGstxQtr$xJ(AkNsNJBVjz(BtSyrUuJ7WU zzhui^ZAlKaGJaxe+xgz{zED28HCE?wto7VDJvaFCAB>!A{P>(Kxp|c<` zv2Q?roj~MW5;;?&bIG3FOLC_AfI32+xdd!z%Wb&YA59b@0g(HMfl`)A zh`3WP#;G)__*&_Y(2Q4kg_hCOr!T$>CSfYP_0U0DEhqIvIx=j=P~@y^M(Qu>-rYPQ zugHP`2kA#S48~XHu`$$^xqLs#7-E?T<0!@5^tCG0r{7hB8EIeLbpAf&X41GU_26B_ zBO2#*1PZ;kB4Qw1Jy5CdQ)IYI=9-fD73K-I)qiU;$V+lyuS+m2>Df%cShW)`DoW>>IQj^8c8iQ$iCIZs+h zR2ZkUoc7{tETduFW_h2X*R!vZWSWl^(qO9X&Lc4FXJB<141m`Jwd-s(M49zqHoL~+ zj*PG>l@GnTI!^(D8|;{q{UC^UAJ4di9?DQuaQ0#dA1m4}n^-RCLUfSR6aj)NunygW z9>)3(V0@v2mt-FS0r=qe*e-$zd#j=e*@uf{XFY;HMdfri)4S2QHa~V79aIEi1g7k`KR{CK|qomm-y6bJUdq=L@fVJa$WC2$S8sw@`-8@jV=> zzaZ7*0XJ@@neBIgToY$D_}l%zpa^*Eh49!uXMa}nfg8R{`Qp#!z2 zZA&&~$YC$xk#d5XlU>r+ftB>22ABG%2ADFdA=!<$VmsF18M!6PRKEWWgA$d>F3qXq zrT)Z^pe5DuZyicdm3K|$jhxYp!kSUZ=j;!t*xOWg#VsC_0G0qjyxVvF10#-!jd`%3 z<+5>1pTL*doKH@M=Kyhn`{fh}ZQ})u??@C{niy1BJ2N9ciJmjP!PA5K3ysJrJ!~Nm zjDx)Wad<|))oe)RB%vd0VORm`2s^hv|Fg&vuLYR57s$rd6h9;wPBvjYzkm6_2QaPl zVIYjI^32OXxMv9iNXO!r*Kg&Mg=>00aMpWWbsmaWmhCTRd|O+OoEQI&q-kZFKkT4} zWc#d^=_#_DWHr%gR?6mKdH^g`r{o1KMlC>qMnV^6zX&4lsiMS7_r0FB_z=sY+?-3? z-3^J0R^joPLaWl@DP&F7NvsA>N7t*%ESAZU=*-Cf!J;X|C=7lDwY~U8isetNchj9X zqLYun&|wK--yOu+zn?IX42=g4HV;tfN}oFr@K_2Ro(i(=V2?DBG>p7uT4;3 zggMw?Er;t;hn=!gaUiFkU|6PNKPsl-^8=F>gwDb&W3+zsUNuS(Ytlcj0rjf9*Nl0o zuYD|m66cVeUo`y=o8q|TfY=D~&ZyOl_|C0skI1Dz4J&ddLAU*Xz_&!wMCWZ^=!n@& zzlHPe>`VlNq+TCZQor75doWIhf1p`E7}+*{$;VXk9)IQ7{X$NfYFq<@qSJwo?VNe^N+=q zKJ~7QufdSs%%7FA`38MQ1HaMMI?lj5bsq*@-lty8Q~2@LB2Ut7&kvOtP8p~VCIqH^ z`YZwdZ6od$l;6X43~R#anjBeew|}STmAnq^**u1 zn!z~LqQzp!2K$&9+jP>EInYaf{?5OVCJ2IL9VhSf`Mh1W=KU)4_4bA;gxI>pE`!-6 zGURE~cxeNmP|cXOxFZvYZWRq3)%5qsmOg8$dKut$ov2vW{`-c2)$Pgba9{~NHVH^6 z6i##EHKKDUuy*>N{y3vJK#`7jxcXZX^^g!$5B!WiNQN6WaG%w1+0*8o_`$6}6f*e{ zRCNycIfyAm7K4T{gc_`;C~6+Nz0&z6Gzp3CM!oac!+9kdkuQ0=ohQNEgGWvvtqaCE zGB3k1tV?7=7M+@H`Tunsu$eDCw-$)CkyF338{wv4EZiI=sVNSv?$`liuP(rJtAM0w zxX^-pyrETCqr7w=u=!IyLUp9WB5$MYjy;$1jlo-5I5+cBUPobP^x53|>xioJ^himn zh5%V=(fl9-Wb?3TkYkEoS%8y1bF`Ie{NIOpJJQtT$Lo~8>(o^eXW}gogVt^|4Ibzo ztHGa9^)B4q-GN4V&awZ^P{Yy(g_psOy{Il@>J)q%jCs@05e*M$@G?^LisaMTzXuZ5 zT)T|?xD+37{M{%KxCvnH7B#cqBQv1qz(*VC5T)i9+$nWy-+T{mSf31ZV(6}qQi@jo zWl0e`L!Wrf(}O=Kn1tFxsRuk5H6%dTG_E6i80FhU5W<@w>+@w9_oUS+o2t zp`Sq6L<;p_pqc6;LmS2d?l7)!L0=@ETKO-{Nr3hd@^sdb5g%;H zIBBofxV;JNWx5W!lxjeeM}2~nj*6zcg1_sarQVZr5Y*T;Ty(-oPkc-Ej#)-~HUUL$ zxmiGH%L8mqybEk$?oE0pt=3rUpe#t&l@8~gh3lr&Mx^j4e788`$0ITwQ+g*%9f}Cd;GKePk?h zBigBEs)5I1ZWnj5=R!KBLvJpMX#$rRP^_ikdbH8yW@fP87Dr!!q&FOYWSz!Mq+4YZ zJl`%mr9T^jjYYm3RW#IdetCM=nffY2HQ4R^dZ0?UlkP9@2wJ8MUT&C-PcebC^3Y zCs_aAP360i!gW6{EHuJhX&V-uxxM5n-3+KKtW_ptb(QXkcM7%# z-kjw-8vEbqnjrO6RELVLGW8p%M$#H%C)Cy|U2?o)Cr8~SyZQI-D9jb9%U)9L2B?2@G@3yJSuij`|11K!t)xH_ z%_NgLlS@`xjubMjtqMA=%Kq4U(uyp(Qhte!a&Zxz?;0j$A_ah`i`8Z7sG|3Tc{sHB zF49Y6Ln=FY(iovDSBd8zv=3g3Sl*d!jn!>gf?ZNmQd|#J?3RC+RYc$K80A)}7~*t- zjV2sTt#=Fa7hu^ZlggI_;SK`|s`;QS6q5AI1!aVU9|(yCpf209RY#MGR%dkQSH1c#6?(kR^f+ zKl}4trt2ZBos-9amLsKod2kc1Lx`vCR(qF}+W2;6(XEZUq1qm0HrA}!MR{gRpDQhz z7l-`3z=lOBf<*r}u|?O& zFcLPLW?W5sq2PHZRBhn*8c}j_twPR}%mi3w@`(i4kJO2js&nonQNu$MQxIk4R@x97 z!fV@)7};kB2Qk6SYMH+YsXZ1ak5oGp>8(?BLwzPb>F&)u5V za-AdMQo=iqc3Opf8KdLsi}lrEKRV+%xO3TXxeoJa`~$>6x)yFEGm57hEhposM`BBdVEM*6NOpspTRRvA4?XlF`g@XXN<-1m+#J~ zjN{tpO$Gh)$k)(VUGG}wt$yRGw%de2Bbfs{`wH*42P$}fFz4+1UwC^Q$ zZzScb(_$Ym2qnQm>y07xM;Od`!R75U9D`|DhO4NqlJ+uHhpY2vcn;eX0iw+>_36qZ zI!*uWE4q1^1NfuC9m@t8ZK^K%livj~`S<(_$&SGPxHz{-J6El>P}w<-JVgaf;~`-_ z9ZoV`wg}rlsj=NzKu+@2P2Xkn%~nv9*RA&XW*e~+)>(4WL+_`$1TrF7%Pum}TuGht zkjf#2x7zfZodW=DLcikZf|BUpC;7)R(pk=62@~+Tp2JkdfNg)x5eTVIvvz?dONQ-M z|IJQ|+z-0GpV`v3^{2Rz#UkgU9`X`j#4+8o)^^-7?9FEG7QP&S6_vs@K8Y&=_)1~r zzs~)y&1NDYno&)7LtF(`1!l&Tv#Y&@alt>W1g7C3w1F{E*6*?O`8ZczSc@2={xs@- zs1zKkKzb0x5lop=sPPS4Bwa0KKzQc`kR-}$jHD14Nb8vn!RUbOn&79A9*~(&8N>%l z_yWguG(51diY9yytW$IkOFy|EsvlM{%2`Xz6mR33RmSnIv;f;WxxhI_B-4KaulHNf zOU8~i+=GI9Vg9{rje|Ii*mrUc`bk^*{l4k!9oAQc#7Lvw4TY<*x7%#QZ(S_DGsB4r z+I*)qBg~vtrWJX+TDX;I0N|^yBFiKBKP{om^z*~jTZYG4>72PfapRM*3PLU{B7Ql} zC442Oa#VHaz$RCX8||<*ds4Alp>O1{Hxn=Kz-C9{V{77LX5tNLyt0tTvS>PnJ>G>a zdZ(WINgxr1N4y@e&*}S4Amra#yFh_HIymnffteXTye&>w#jkj&pZ^>j7ULDB^LC$^ zn4PMMs@2Pw&g*Jj z*Y3rnXOypVwkpbBqJ;1xbq!8|O{GD9&=GoNj5OdPA^VcOztwWuTvp4PHMo}00G}0^C`j%334E<=woDu%nnu^d@ zVP9EyiLaZ2`bot7L5JHsn7c(WUlmDo*axI(EY{pHT_Xf{`6OTBL)}pm`60DmfJNW- z9F!}aPLAe_I4^JLe67u2?nRvcF|&(KVK(qVUZLP$|BIK&3~$Cz+_!xLNCxQXae zWxS(pa4g4K7FPpSq@BFMITZcy)Gqyj#d^Ldf%Nqj+sE2j|AsP>13KY*k#NC<bA@wk)kGU{A3w z>r*0qcd##whh$}m-EfDLX;q`A5)GKn^OLly9_kn`qWfaBGUFoowBTQAqvyhUZ0R?- zSm=2LFNlUCVI{W-JATSm_Ib#%fj&wFB;M|mXK>n3)I>6{j%ZJI$ij4&*0)SMzInOl z>>tcv=sdR*2-@ucR=7jmCDH)P0sA0Z8;=RU>^0PfHxMe|16Y`@-}MD&d;sC#$oD*% zzp-E!`tC)C>3f`Gq(p$>O&k7bCZ%5i4rPQoycRwR@9rWW)=o-6*v@<$T&W9R!*&X6=6IL12#T1NG zyV0+x`ekC{o>E5^9b1Oh9$6ZExp1gnekH`>e&_bL{_6K0>DObCIqrM*HTyX07@J;A zZB=Ky;gH)kQ^Rzh>*m4lzM3SFGXAukK|V@TjSW)In`M0UMN;4`4_Ks$0_EL8>fmN1 zL$)G+^;_i!>PeO3W)10oD(pC}=D*T5J}kLh|5jo0bXE(Rz*6E@0cFPrqsDEmdajSD zo12V5fbw*-3Ti!jEEF{chbW}ug;4ZWgy(8swd9kJD2DI{Y=Y(f0!fP5Eg=WSL8-tp zF`kIgT-2RFe3e|u#t+Smu#-^VsjiMB)$aCqaXR?NNXXr30}ZIPoQ?4G*d%L8G8`+pZws*0B?XUc88)uhn{L63 z6X6aJ-)CAN?#ME*4FB-i8Nw`&j!Pv(LcWsXa2UGlN=|^<@(f2&n`vL7e5^&}#39^h z;e(?d-;1v6`wbYrpCOKZWjGF3Ab|wzQ>GYzNa?F@NFT<-`1$%OC;!v+*kD)Ev=o_= zFcVgT`YEsxkkmjZUl2An8g*(-dP0q7eydDVBS*0BsiAk+t~Hg6`@x(9=JwGl(xIm>?U=RU}@1ZgXP94>1^^Rs-mfA3v~Y5=>!% znu-(%=#u>9j!@aP;{+C2jVCjW&KrBnY2%1aw%2{~swh>{7~rktmS}CN3{Pvac8E#G z7u;`dTG0Q(~~=udP($I$okyXLnMUU3csA`t^Q``rVUe(TW8`+zabo8o}_m%lk5w*#i}SdmP)5IePNcN^$F)_8QT6g(PD z2W+>#_i$DKuC_UVrwZI#iZ6wjvLDfk>(%EeZWmoX;*jW%nh_obQF%tglqK8F&XPts z+6G(@exfIzZx_tQR+8N|{7(_u+J(H8{xI0r{NkWl2SA4U9fAKsE4R0m-zEgfaN?)7 z%u#{Ghnrl77~pLNqOaBfMN~@7DeNoBu zTR6}dbKF>}uS~S-KAS`3@`jJ_$Yhs@#G_o0=R?g6Jh-awNj>%_QFv70B!xUwp*|b? zv`)Nm({o+WOwz@+zK6-1vOX0HfeZu)r9PcHO^ZJWW}HZtd`}4WaB$sv1S@oo z84f;eRpDw|+*OoQgJQs`-|{|%H+fx+eIl}qN13zEyL4Pvovk8+B>@S|vy667d=WsW zHd4@nRAB#5GzMRI1{6qyt@$|sE17dlx!e>P;`)6jDg1W`)3IS*nSioC4C;0s_-Whx z>j1u{Gzh=NI`|p}xv&OV`>TO*R!y0%?h9kxSbDx;2RZJsS3Q#rx{Y070&K*z7kJHgnrhNX8vf-~E#u;!o;i>-92i+{_fs%iJ< zXhe3QoAWBr@q1IR18QIS#Gg3OZS>8rN$WSeWurzAcKBGJSO8~ZlPd^~l~il_EJdI5 zgRD-rHobmRZ`q;2VDD<;O~+hWIpJ(`#yY|?+1R8b)2YhMr)nSslYx{e%6j|fsEOnpm+vEto1suqj@x(aFDD} zCoI?DvmJ5|Is1j8{+l{^>gFZT77$9YANbbiK(_y*0iZCn_ASq`cI$u$*UVVEfNk^i z@6^3OYx!H>T%cx`p9kk*qV8b(=3T41T;&en4dA3pzSZsZ36A5Ziv{?V8}uC<|2e0- zwV&2~UFnHzxp(as7S)G8jFJo+;K3mAd+!X5Mzr}F^ZNz5 zsgUkm9B*=W9qM-@92oNcLy+u&N4-H+LsnJ8GS)HDkt=jx;#^uG)E2O!JoSVC122e` zYEofMDk^$Ub;6k&Kfmh1RU)QwmzNoP z11A}YGDvVG^6S81U#+M7Bz&{{&0bQ1QFP3@`NGeB>{_~@%4fHx+(rUBy;s@) z{j^ToU7~EQ?L`;GYuGHYi0$JL%7bS(w3gJSrLPh=({%5lTV|uU`yN*=Ngk~@7QS3% zTVe~>b-%$7U7puza^RXTzD34Zn=x7Z^QPyAKbN%!gd(Af7*L>$sk#M`a=+DSph%C$#vN57lpGW zF8pWcV}=_5$V!9szULRt3xCBlccFrqw;XBqIPy$g&)e%xQ%Gm;0}H}hK<@&xK^^*4 z>HeS@=~@@-)cvs=4oOxk_(6y&`G7D8@4r9woZliQ%CuSSAF@ujSZAZI)NoFKbv95Y zKNiH$u-TfIXF z`QDCgY_9D0W02c*ypr$to~nJTiG;$me4XKv8B*SINs@0)b|gZsIAB*@^V(8JtjdgR zLg0`i#XKkQ{R+qclW&HA9!2$-p`ZGG+!<=WT5uJ(3U&W7Gr*)ZjovKSwDc?KO)d4f zgG^OK;%|bu&_Y>{nMh$F^dY95nPvWP`mg_{*>!$iBYbgyQoVSRn8`jbb`OX?6}Y$? zm|Hxx%fIM9E0ngv$pqhweDh0o3{Zpm_S~@?ruH&E`hPFg-7&piBpwSUZ%MK$9$<+L zHpQL7(_fbWl81IBw>tqy8?~|T>@F>aY~=C6@>4o~qg1%tE1$3=$=~YsRLT)t-7i6o zAPC0i407+x#UFCeQ<56-tDrP_tCh`$L*Yo1Byy;YwfCH8jA}(Vx08hnuSGM` zvAfjOed@O~mL}4w(49?ZGYc&drUi-oa!Ni(oRqRoWaWjm2?WXXhw77!QU|N=lDU?X zUAT@9?HxEl+!uD3x|VDB3rY-xAAIf)BF8p1IhZbjx*<_botnpr`wLrj%tgf|g(*du z3C9{DJzH)|8b|oG69TRm9}sbum{FaC6^_;T=PJ1){E@!_S#jyr|qQr}G8t2$k=K&9N1N zS`0TxVfcHkT|(Hez00hE+?69GSokPJjot9v?#Xg`NWa$L)g&SRoe?=Ue&}ybnCNc( z5QyuLYmPoJCH*iZofi-u%jyVu_(B>=-ULE?egEARb@Kn!6~v04UW82i7c+7q^T`U!vi`qh=BEz3{^acrthlwizc?KZ7{E$Hoo6 zo^YMlAx1X6-GS7AOe;!ecr+J)bk;2Js_zx+W%z=T9u!<3=# zD(FY`sW*B-lKVcr+I#m6#oNeh(5kuP%D_5#a;?1hQG>{$s|5d}F7q#gm_XLXO3CBq z9bJWvPu)GIXjg0dXOd>Uh0{ZOgg+*x$@ViK1TB+4k#SzZzu@|cT&q%9r>G|Y1hvlt z;p8~1>Va^dh;&?BBh+Z&hv=DYp62u#m&7lkQwmFQJEHgN2@9`*1E z+2TZZW9}n%v~)xqdLRScX8$-UxAhlty$8lB?E#fL$-;AjE9ChP!mxDvhEu(|?SN$qTAELTNNLu4K%Wf*WOwt4M=v~vgn+&*&84X}Ma&2$~-C2XyRsG;&Ioi9A?dK~?VaXl_e zAh4esa=i!Bn};%*N|9A$2xRN-WgpqM-OTA?zjie56^oLW%Li^GzqQoV=0FNZ;`b%7|z9Y!GNZ{_o-2(e(@{0g?!B1G+PVKbXPk zJ>EEy_edJicUuqyp-`;YI~MhNuzMjvP8gf54Dv?2I+3Yr`l^2FdvbYv1XMt3bc|k%g57ieF9or0HkLA@D%#-&%@vb#`x}utzg|dcw;lczh)-W22O}Ad;z2Ywqmb9?D{>Ul6tXh|2s<;%XmwCM!jaNQQJ$4>g700vI$t<)p$1pYrl2u%VU1eWdKW@~60CXsY56w74Fe z%;nz?hTvC?Ab_TB}U_KCWDcovD=mT9mAP19D;Tf6igaf8hKALN#{< zF4(HSew?!UEk_)D=R;5O3-i!^(6E3~jDSxXszACFX$ds5%@tDHmEqVt#4Gt6jVsT@ zI6(^2OW%Q(=;!b_NSNsOg_e#H_(tBIC8z?E3MJcm*jG`#a~*u*m&YSOn^P>(WL|2} z<~Y_Ozm610x*8VkMHZpV5g2Lk!70uUE;kT0F0h;1Yv(p7^odWbf5K~U?Za4Esk1K- z8!Io-O4Vq2XJ;Uch!hS=f@&2Fu$lB5YI?^}s1rKcmLFAeHDN$0i(|?9YSEt*i5%(@ z`~Kr!?V#pD3q-`#n)c#x0T`f16Ol0#XK=y=9mXxMG>d@Vv#M_7TVCc*Pxc1F=;yCl z1#i)0T=ry6+JY7nt3Jnu_#IZbzFL%L?SH>i66hCz%fCycQZod`|B4>RYp1YReDYNV z{R51R+Pj{n@4fqfA0Jr)n{Utvsyhm^TY*7y-GoL4c9_Wsjb59jIw~hq%6GI)-X2hx zQ+J_48eZoOnBY>k!vyab=*oCjFB~6w~j3*mfyCH_`H5B5$db0>PMnFs(YdQ zyOHfQgYy#+T98`h>zHMkyPMItZp$YS1M`%>qIU>Sgbk-aGjdak*|oSqwJlm@B?GG( z0B4=fDTI(Bul#${IJxFgl7Rg0Y1!|bzOaKtA;)_u$}|Cav3-1UYV>Ecez=!3|3&9Q z=RA~eXg}2z@HPq?@8GbL)#lJmJBJl-=MgSU+*17m%c7vQk?1zdlE#3RQ>3AXV}7MI&+lh){>4XshMm_j$c$GSRz38_bQvzY0Pn_M@P5L2 z&w4kZ0U&_H@Td7teNmAVFX;uWcKrU<$eP3Dikv4#vk;8|qA`g~t-R~|-u>d`b6iPb%sCwBcNZw?!qWl#F z1dD@Ym@u!4_B4npKI%CWat1Sr6$%}dV?sEJI2|D=CbIoA`EkGwV7Qsd3d#8A3#cJ2 z;tdV!-H2}_?U#$iF+nc%8+^^aePgFl7#$4zF>V*NWr2eM)IpipJ8KUY~S3@u<(#oRygK`Rs-h_c@160r~!z;BZie zY?Oz4hsYh=z)F*A^nMMBFi0A-kxQ^u1Eh{lBNz^GH9d)2Tt~aqi1K{0IE5L-Pl@d$ z#n{#8M6|U7e#_NulKR`fI9mEDzsuBAv?c&<}~bb-ef|=lnD4e@z_!>qH1F0zQa- zbk#aF&tnC~E|?(v;7-8p$FOo-pdljkh_Y!>3TVr2*5u5ctVI0Va1|6s)^wr`rD{{)SS0PT!S}rO4Bzz9)C%B&kQ$ zb{~wyy#IW({ze$F4q>nACZ^g=1Nr!Co^cinpK^Am(I^}>M*z1FBXLQF2!bf_p#L87 zJ3{GUY^z&?VIXsA-oe04OmwUQyu2!mU%8L&+je$=OaC;AaB1)p))wSe)NrDOYKG^c z1zsZRscjXj%DUH9bj363|L8m?xIIr-Ixx@&gcE4;mLG?ZK9JZuk8{Bkc5hlKVdeUy`mRWOi*5F zoV&*r)2HGkxGQwl^t%O{5H5zDIQpbA&5Ba-;&Wi3I6G@<PSPM%}K3Gf~8-oK|fDAk6`VSt_a2 zMaXstzDYFDtXu`ev7WcE{Es(!8Tcr?!Ubo~GC?>NZ3ZEVe3Z{%H^xv}1dyNG!kv2B zNFL_}mr!;Z^vR0upXL8D{-@O_OO%=3m{9T24hajF3@Npb1e&XWZHzN$nCTb?D75U_mY z!S>?}-g+k!hiGvJk~;)BgS{>`+m|l^?B>xGH9?|9SA_Zcfhfwqrc;2&YV{-ykp)Z& zNU!MkFd^6KX??gsamFchICn79%JZ3pI!^DKYzqhI+;@I>w3oM`5d*`{&sVBd#DAXi zOX1u<)ji7Daj*>9)8PG4KUd>{3r2s!1kws`1G}|<=7xEma9@(m?JiqZX!Oe~exwz0 zAuYp^KLHko<740mHuFH(;jPo5IZoO?0>YlJuh}~z9f$TcLm#uMXZsPgol38Di;0g1 z-DKNcf}6bWl#?@lrrl!9!Ok7fsr|ABwG6@qJRZ=z7*pWOc@iG(TX&A0R+@!9W~KgT z=#8NFN%;v*X%Z$)g}yD_q96cZq@^JKe$Oo?86615;bN;mrHR8}2M6cwok79bQ@Jab zE)@ksCc}blC89$0PtuP$F`shLq)m7)da*ut+zg%4*%kiSfV+=e#lsxDB!q11{#K1r zFIKapUm+EplGO2=rwoE@_j&&qSqu&N9PxIf`KYpN5zgbwcpW4U2G89{Oo*R^?8h#9 zk0pz0Nfk`}o?@Gh5oz!3W)Qd(1hGSG%>_Y9^+H2>UFyqi=7mzurvCV}-0GbiNOtMn zd&}FfpEiq_mVe?Q*MVm|-uDRA3o(-==;yPpL7V8%_Aivk&1k8!S@ne-tWK{no!rmM zKFyj81sI}5X{qIM zNZ6#rg24gcL7R$^?mQ{5Wtetth-C7Or&z;7A1lHn)C(bV)2~Y5kAYCCeA`kQ&Fp>t zcO6#Km8@Y{TM1HGRK>3RACz0Xf~%HZ975E&8Sf|86OL>4`_aAa0aI(wb@H1nm}#x5Qm$B-Rr+?X$~So zuqkLZvOhUx&xKe2g&n(h3$Y09WDNX0pY(axNZ*LLM@pRypSF((pFxWZ0X=X+tE575 zb{G)HiRXOkYmYeCE)f-RhCoq#9x;;lvBcRz(T2Akcvw9y^U8qcT(!(_-~$>HosUpI z?$q1fgNn5HDw14NqPCTTqC4@?hh+P{||~)AjkZ91DShIJIEeut2T~fpK2pMvqT6uj?=7SMsRpC1^Df zjUR9yF9ji9$9cA*RAIXD_F#0HvyvHbNE=X zUg~d4e60P5#%Z&J&{w2Ua;={_O=I=LDE&q1&wyy>3DsABNf0^<%qlH_0b(I|Zv0-P zfzT;Y1Syd$eWglZLpuyf&SMFL2B0fsF=mlyW7AEYobzdt85wYpO=eZ2^dp!q+{c|D z(eutRXHo7{N*)|y$*BCqt8s=M8|}IK=B+XEI|8nyAmaLSpx7g`!Heq6XbpxYViZBD zB9`d&zM8WEd`OjkQ%z0#S3vlA-8yFaZzr;@4F4&v4u-WHag}Nrw9e~ESg0^z15w4j z?t7)(r*MIxiUCn|?gw2-lTeys6`n2Eti=>o^4n*&RW>MpxI|BaZw_5I7r4J=)~JlkjGQmIZ~P>hEbH~v-dQd{-|Bf z!s5MnkAC3&HnEmIAg@wk(~3%#LFT&=jWxw!**Z82ZNch@fuY`th#|o_9R5?~o7}g? z{LRoUOlCxgf|~H)!3{!AW?E5wVHj*YbZi{BH*^xL=#RFOZhC@&5g)E!~Zhg25A%+|zumAJ?uf!?IO~8uycc@7L zm%0-cf!$x4D%U%Zz4WU~6(3!Ew0qHUr%h|l(#RHEF01$D_O}xLlT6@zX5WE@_W!1H zSmDlDffP(PayawuKrE$(gfe7bQ0RZ-+AJx8=DIfHWo_dd0!9185U#u~)*~9A7WaYq zS0#b;_0D_YyHC8mXu^_?^Rfa8D=lLsD!RXyAe~^C<9LeoYFT_`E@Pin?TWzVj$nR(Q%ms+^Nk^$~v0CTj z{tavaSL^%)^__BQydTF{W1CvSh`#BI(A6kM`rItw$;0~2F#7aIQ-=Ytg0wSc){1`n ze~$$l6AT8l6Dv$qpqr07^eV7KL*Jz>rcD8uG~Y#K+xH2IF13Tuae)zsZDoESoa`3_ zWISj5v$6n&5EdrP#oLCCLtxM)!4pn_Oqt8}nFHgN4i&kd(Tvqs8Sa zX)qtcyd)`*79OT+?Hnb|dmjMTwcItG<#G@x2eQz`jywj2{L@ECe-ujXmWH%9Ho~ zDZ?FRoDf-7*<5OQPGZ4!6vH8tWS~13L|`1%@Z3%>W6B?+GynvGxJ|eC+}N&u+?E1q zg2+^oxhN6x{!APH*G{|AdMi6Ai3!7zLR^;-sisX%$k3FV~j6g>BDMPd}EUW7Oiupb4HL; z^zJa7NWii=aKCd5nVjULtA>4PiJEFnqA|eA5DY&vM4_bYgUk7~ABd@4fT`f0n||KE zUH*Tpc1BFJ<7Lm&fyaOERS6o1QL_aC(_!xzb0(aRTx{JBeNFh~V#Kp}Rf|p&;x(iT zYa#}Kn6NL6hOiP$*Dzd@UpI`!m1ea_3UlOR3Dxg`t&GcUAxRX-c8$e_Cb1rhTLC#9 zZ^HL_b1pW>>k3Lt=6aV@9l(P(jdhCRl{thk{`_+j)r#OL5vsl157f<>GDHzWCc^w!=HB`f!bZfETojS>fyZhXds!xJydv)F>zmTUzs-Xy>lTdgDF~_ z%mAu~%X}~ikOoJnxW%W6R}8qDZkJim886ZR9)Hg#>!JAnxzCWW^H0-&Y5jXri^^fI zt3V|o2aMmZxt7I0B!*e=XN1e1$<=wl!lR~HG7=Fl=p_7}%TjfyJyYEQFF4$aOn2<7 zq_mc1j|Z0r=i2IcR_U&fKJD0@bxL$%b*<-Or=XxIs){)6V7@eum{9KVpJ+6079W>A z9ID$9Dsb-&?*C+%WRjaO6+t$;CvSN^xnYu8K8_2 z(TTT`ukXd7xY~qug?fKpj97ReIRkzwSYm^hbVt@kV`*FkHD&%Fa78krmb(*qHtd@D zT!kLnnV-y86^aIfXg!dK7s8-Bz}PARej)2rYM{=oDHlhP8SQ*KpczI@Xd3ARvdnU; zb1X)QXJNHE;ofgU33`5nP}gx8WCH%t<>@ZsQ6)9iPzt7{`0992lnG*@!zo(YFHCbx z7g#;lKQNnjmK5$ffxzgwHU;Jb@nRgm-kSv=Im;yLcgLNZnvL9kIn+NMQ(#X{ z*BNFCLAJ;Oo9I)S6b6|y0BqI_=^HltJ6Iu5na$GahHJ2q(rqsXfwl}Z(WXKtJYwCII{ z1RijTcL15dK5ZmS^k$frW=}kk@F$z#tAY%uV(Lr&WncCFzinVh}Cg5ORV*5f|!~q{?SuoVHNe?^68X$YxZwXTy7-X<%<1%Hns(r_M6=mG?XnY-U5oJjx3zPeevKH+Lj05e-LGGu_!xNOY@VUIBRht2F;H~rSIywX9 z5IlLEx`W+H)!g5-9CDYa8e9u>Q5t6u(NgVA>b zuCbXX7tH`h1aWRuz2%lSo)lv=S5!5n{Ya}sxdnW$Rk}_c=GteA`_bV5(~ymrAC&r6KQ4>SCTK$7nmao;CbzgpQy!wz zeNzOkMzkAQH0E*?^u%g9@bezQLSU2o`vvp3N+OJA>i8PY6d2y;p^Zti3pp7( zy1+yMXyuFbD7r9e>)<15^7obT8Y6tKlcgge`MOjSOk&6rZCId=C*^iB2pSD`2^1!G z@aO%`CyfJ8sb=KgUzPIl7}ilHrH<8JZPG zk^hAdh(h!p*Vz+b*Il(_Qf3`~zCFg@?hR+ac>=3`t?WQk-;QUucP<$5hV5{z+bWGmaI{KJuSNB$qe>L11U6+p5yRl#9j`?x%OA`Dg71zF27l8`r zYIr*g1VMub{J>6q2Ndi|;q^WJ8mr~~WSpHd|*6h!+ieOfn9)ZV3a|Lgsj zvaXS=eU(s~9lm+PL>m$)f(wS^JMH-w0#3Eh^tofLuhVt`8AfFjPjS<}(2O@g2$g#& zVe!q>0Q5PGWFXzp>_)6yGWUFcK+LljP#tD%`T-5{T^AlYH%CV!UK4L%1_dcQW`f@R z1b8*<+0(0bK80`ed7EJ@meCXN-I%_e>JofV8XyGu0@(6<`MV{6T8D_*FkvG??{_#~ z{ef!s8VMpwiN(F!|D3kbiYHg41Vpxd4P*dRgDLeVdzi6z^?`}p^PWNKvJyRNX4Og~ z0KOx63$We-&3unr0$&O3x^` zY0BR=vraP+AG6bPG{p%2t;WAdipsyf<)~>H(q4urUn{Ugs%!c!GKqyvXx)*c=%;Gy zm*ENZks~7fcTFY14IV=2UV!~o$Bn|iMri{A<2>E(<--!_;V*VGk1I)IrL>-Ov&a3M z-S|PYC{9#vqSFrwvtZvP-E5Ai=Qju`!vtq_$C{mRJmT|&J2YPfrAMdr$2(q|Ny>ox zjOQ^tCz)QR4@?QJ+j8Bi@W8NdHs1Osa^VUQQh*7 zkuboZ-yZZNf$}gJ{iVH3YN|@*hC8Re8u)n&P9-DWB-6DS0Eo%U9nC4#D`Gpxp`wP_ zB=Q8T^h*g%);07tJh@d&Tpv@?Ba_~v2wJTC*lypSdH-(KL}fLQNsbN#m31pz=cjW< zHqx0et+CL@p99_D#m=C6?AHpO(AVaY=eWSJGSW_AbPRz1q%OPz#q8yXD{Mf29%@$ zK3p%QFh^i`RNLb1_(bptDhZ2}D;wG7v#Aa>ay1$s|Q4!JHzFfv~gi&>Dtrp*dvEJmSm6Wj@EClNW>4K{Mvu;&(FCPc=b8*ROHKLxro8FJRD_8_VMzE z&^o?OrMYi@0FFq^;<3I?-YjA6Njp9P#+;A#f>Qszux`iASK2rz*jF8i?q@I+_kZ&q^nse7YBbjy(IKL>0;O0vI? z30#hxH+6OXVekV?t&!c!$A)tNZJ1*h>+F6$D;N~R10;$2F0=R)D zxB7d*cv|68yPO(>pc)(5kz%5jsx@C!`BYuX*^w`);P$E?m= zEvon2rQAqOZvKuc99UtI8;3Ltz*bhJlG}7CE!yHFoa7^;h2u0LjeP8{qeh*l_isPY zFR3hFDEWl73OKJx+K2Vridl-=bIzGQPx^-hP+klPq)vC>uxy_2$L-KznozqjqI%Lv zJUBmGPX~rz&P|q$Ocs((J3*gYO*~F!_6;-ri)8yx2mmMW>|UylXVrQ#Ss22;UxM~^ z^5E!zWWqiLrsCa^%*X3F#priRi~#_->c*Cy0P%jp!q_q%mYE?wk)BF3#eN__2d&2o zBaPs;1CK3+#-y|t3x_^q7ERw0@id-n3c%nJqEy)4K*07D50!E>_mIJuL?Z3)E}$N= z`LV0bQym{l)mu-Os3$&V3ZW!_?*)7Pj5N2-k1*2j(-iZ#FTi0O(njZ}!b~PiJ_D9ruBA35pvT~#9loFqk#nM0E2euY` z+D_IHe#r6!oW*$8vS#CRyau~Nj%dX7y>2)HwV(&{5iC~5*p{ft4ZT9#X`_V+t{v-C17EeqoOB+Gpe zS30L@-*+it!hcJ^4UaG9K@G}6r2*BI$_COXg=lfu7`1`AIsW&y5E*W(9@-)+Io@RI zg2?)BNo9m;KRgL}ZPfOFPkp(X(~YghkOqDED;bF(dGqmET|_K>mI|sh4JWiT;F!bimUc z@x*62f>-Up$({YhrLUdY3CjIfmoP$#o$ptr_62*duTMA2-3<)#w>@Cq z@=0f-p@!EK?3%Na9ni%I>$8=1xLAralGb2AXcdA4ksxbhP=;mR=TtrrkU^n zeNE-^d3@?L^LK}u03L|GBq)2qPjPtSvtlDaCU%>S388sGWjf>+*k|O zirJmtAWUr#*`3aBq!U-NG*+NXcsmDHhAYgS zcv}O#6ImjaKH*TMKx3h_R_kapEvf0MB$&Qt&q87pcSww*bqh?4bpT^ejmQEP^84F) z16h4{x8_GDhef^Ft^lETQ@obW3?O#kPgA`touzoYD0V&F<)k9i#O|x!^%Jmi1p?Gr z?GXwdnF3yX9R1{(4*9c90Rpvkc%sIC3mFNO4xquHV7I83gTVnC7$T-fPS;$t^q$7G zuZ1B6nUwJdnwV=6OKRwhjwKWOcyB|S&kYK0Xd$)fX_cr`r&e@t6T=#wZv0mV@RAqB^eC)S0Ik8Wai>4MU!D zfX0?-^71rJqt9qmCFsgj9PyjNT z1|4eD800-1^X#OTmLSDrnFqzHGm;TlQkVraGT{dOqRMe7=9_6rv`us@IWFjV(qfKu z5)E`P=20keGDFD>5@i^+2vl*Ubaq3?SuS-E$yx)CdB0n%wIiIQGc>q^(54@+`@QsB z=OF*K%(kq>_uHh_d$M9|4t(7=QDRf|3O4d@tus#UQqUq)7rP=} z(wE%U=wiVDs77#UKif|9`Fe8r?6oWHijWPl1J4id9^aa=GZ|3DL&RMcO$y}Tpt{oQ zT_6>XRrTb$nZQNBLz0d1z3{(H%YYI_3e9Tna`#qAR*XrIbzM?x^Dwd_r8X(A2Dx_p zZG&2}YDc2)P1s$n)i|*u7AtGbbr~FjUh+r+D1HI7v9M~)b>+yyAQ72x2aCm!JK_?1$N0}Z*&C- zd%J;RvbE4u4;F%qhOO9VI0#XP6?f2Z#AY(ghPox=v(rTXmH6EDcs}|Wzl9g_8%PDx zH;@_T(lOS~P5GUCf@%}bu)cF+66K=WgA3gR57!rf!a!;6(bm z*6(ITCnQXhFIg$M$%F&$7i@oQ85&8B#XG$Yq&rrFri%E_(UU~>z4}7$?~DmGuZVfN^?dZ88p1$TytVWSc^D5u)EQyu9YBrZgT!u^K#R);vnkW5 ztMy~YxCI4d3vhqBuuCSfsS4q`I9K%OJ5)`Y(!Qrs9rcJ&<$cBB_(h>9xEQanJ zbf>+T={+xA_hI-?UU$M@$Ay~SJr06^AgG6gzwm{_TkvlT-c&G%*UTE;*x~U8Q!#0b zF(&@i`F_^gAu8$3q{VjOi9{Vi&{T{kyS7wg2wh?bz3V$A_hz=IszOVsAWc@o^881% zU9Ld3rk-BFxm%BFVcT4%%;(i+WrG3Ci+CA~yPa$?pJ%+c^8YPopNA;5=M&!w?{zCC zZE&M#d6a1LS)oU`6EgL2kHDzJ_ZU0(!rxP_4)>2xF4uR^L5N95ynM1}=MjalV=Dl6 zA2|j#TIRG1a7%pdz+V-6J;OU1rw6&{L50=lcYv8Nug9@rmTYtfEv6ccKPyfRXx00G zElV((sd1E4kXbStM<~rol*iGT&g~Z5Ta%AN5OB>U6&>v<0hq+A)(e3q1`vlZHH$zj zT6G!Z$jpIXJZtz+`yB!Q5nez#cQl)XAQ~C>28TX-SfB1~`l`;yMf_o*i$`Gkzy_yM z$iZm}WS37KFe5HijXnlG(Ljz9!l>j)Ow$`(tDsz`b2`fzWL3-C#z?eAh+Hybue08cnMzF2Mddm zRYEy*Pu8+6WfDncs0sO;H-mH>Z-9X#<7Y*#Ve7a8Ou(J-(zzD*sYfIV-YNWyst{q*7 z>`=fUSumMOoj)g2+-zLq>Me70lQ5LKNgb3f5|o2I$L1gp3_TuGXY#Y0_AMZXnA6qy z5~QP8-NiTvvg8O}PuIz?tz%#OVABkuuIK)Ul;+H1HTJ6~f>Q-|nrl1lrp*7S9$<1$ zl0eA^j&rZ7qxfhF>9%RmnqNRvTW^S=_@J~c_G~$1Jk?zQg~!3?J=jq#8T*-C4}3Zl z(Vo+yd^?)S`&XiXgpUrfy+iGU+apIbESJ}wQm0&U4uBvnFS zKU~3r-7<%rL&Ti>eVz(9aIoKmgbs(MB&tbXJrW^8n-WV-MK(byRz~M<@uVFvg>BM%B%D@6vgH?FaA=&**EZI$Uy9CKx0)X9!i*Wo*E-w zHxUp`yarG>ntbMl=se%7p!x4i*0w0t;?@ZQ{^O3`OA>h8c8;*6A&^i z0lQ|u8ho$g!!;S?1P5G3CW|hSGe=*>;XP{_q6)pwP=(c8Mp+0WS#zMPU87u^X8D~h z%DbW(k9Y2?66$n`dcpqs^YdXkwPHl^29ESB#m_*u#f;k{OJ^XhpKe6w-55dEaJOuZe%l%bu1XB(wCLxgJFaV>>#6upR)TtLTzaX4+ zteubwxmr9g*;hAOipsqFy?Pq#zWAmgC99D-5BGD$qa(5;Bn@}SoF?IKS=sb4%?GGc zV@Ht(t(A(RX`BeZ&)v3<8T=pJ&uLAEA6)6R^c0hF!tB4zcZ|1fS}P5g%P&)&XwkGy zN{}))5BlVe*JkoqPy8TWsscXd>o>g}Uk4mBg(e`0Ng`wTpo(MZP=kUjwTy$AC!s4e zCS^&<5-e$Lf93HnL{yq6t-|=>DBbS(M@$lvtkeYbw5xBq10`|JP1OGWlmZ2x>F>FF!iH!*gmI6TSAxni zcNOi(8#aB5!Ni@fH|g;x$kh4smE@Zz&llJM`~(9ZURB^iFM2-)jE!#kUrj`)@xZ(cK41s@1Qcl6cAQgRA5^(yT$UPa2aT;otOq~T z@lq%}bwzI&0`-NKlIsMw{e=5da_~rbnOmVGkgCNC8YwWvww)DZ<Yzq z6>5aQKLes1`=#mSV=Z?y!{{hTRWMRGS+jQh11boN`bS|&!*g|M#c~RsdB0@I1bTRq z&j_=9hxp}^*&C`gtS;$Gn28&-KmE=FU<0NquGcPUz+9gs=ph8KuvHrNq(ps1%S%N( z(K@;mZ}sK$L?jK^-7+0?Mq0S5(s@m##6JFM5r#I}7N+A;DWVLB(@KrR_YRs!3jEMG zT)-<6^S^sBxvNe9s^qI9)b|d+v}v6u^Bi0sK@&lnL}>b1OIt~rM1c{2`JD_OgK=f{CY zBW`kPMmI%(GboMVCx9Yr^u_V_12RsxfT+5_+TADpup;y?-}}O%>GW3@g1!!5-He9! zE+1k?Nz-XR%Fd$3Z<|w1!-L_uV_JdsRuwt(pSQk1ll~3}epKzk7r8pSVC{XQNhhUN zNRvWBPzRG&^<1M0r@NLYTY@ryY0Sea%(e)YbDj8=G@Nu7rQGkRE1L4!+UW+^ng!R( zSPKbXjF4>gHt))WzdJVfkFvM3dIgkdnnF(=h?SQr2XE?WOyugzgf6{op6S=cHgPPq z&`LdN9S)8;Zgi~s{RT^u`kag+B;lu@j^RIW=a!e3GmUlN*TgVY6CIn=AAcq8>ej2v z*~y&z=B{sgr?PRfEvTF9GKZ+jOicpZ51^4wlXOH2z?i4)> zk(;x>7{a@hzx|&9)fv|uwL4~EWms=Vp8KBoueLg1ocDgK@H5re%%Y1_QB!iIswrcX~WXZ*n;8$XG;~A&X%B7M{W1NEmPB;z`c`s?5w#k zaK0w!1S+l*f@0IUzWuX8N8_a>yvuiV`qP3gAV?-$>9m0fBUScBmAESGWsum6rbPyWq{LI|Gp944||3VU>dW( zNISizM_>!w{k$>G&tC`o@-z>nL1v#$P6Chpn)}n(<{T5`w^0?-fJ$x>Z{rX&6r32% zwDi~_26X-8d*$r0x+kdv0IlsEkXaC`mCzkR;z&Vj>;OGUJ?9R+S?i2u2|aD2v1B2Y zX$YY*qQqoU5cDJrA|)<&&f5q#?+MP@1mVP)gEXQ^>a%V=!e`vj?s!Nq0Gh$CM~Q4y zd$x^}x|K;Y=qfuIoYBva8H(HBiiBk#Q;D-5n>2f%MSJd{f6 zXZ5RmUnaM1p+SjmVw~BoNTq~VW=r&(xON}&7}c?I>@L4AB1<3@C~~TGYpDYO^*}6l z=uYA_J&sC)^P*SJ3k&8qOabDWbbC??NL2zQaDUYc#d)GnyV~wx1Z2HsT=Fg2tRh54QAU9%q!v!yp0R;A4nHY|XL%rUleLp96#2?9J%9cU(LLec9U`#r z=t7u}Tmf3ekH4Pu9(GX_USSfj8P3{iYk^Q5uuqJ;v>;MyMT%owz)cf1Ag7y1RJw;`}#oQWDW~2-Bp46OCrmYi_D2hFd4p z{Tz}-GY9E+sePG{@T!=KzXttsCqP=Agfp5l+#GTFJO8h~Ji{-a!o=G04uyb3JU3 z+qU`Gr*f}mc(ZvY=tIoWU}R#7USWC=UFNNx*htBKW|qjd7iF^pwPcIi2ev4=I;YLS zFVtFu7ubr$aTF3=;*&yyQkJY+WF|5W#3foVHPy6gEhjSg z%AawHZ_$E7I?5Ii0u}-8jKp5F{69N3g0i*>@PASGlI09iNT-$3OYSp^mDNoC>gby| zk?Y)*2RU&ec#dbJf1IPcq>Bm^f6j{cV^=DJF0^KY$3xsLFqcEwgF@zdGtqKRj`*0a z;OFuU+!|tY+VvL_UU32uD=UFMyKzHr#nY9{4PnSQp#inM<~F{?!oSEEoCZ1as3+xmEMvV0bq*wBB9uWqK|C^Bs^ox3mLQG z%}klg@zjv4Cd>3uaPd@;5)82il(jWODT8>U$qQt*p)u1DJw6hkgaz;rLEW4ckQFj? zY?_4(V%Hp4!jIxEfk3lIF^o%5R1A=KL5!liy`fl{Me^t^bNLJn%;y8bx7!5FO#5rp zdC=)fxZLf=#7tjM?0%j7Fg_jar&+Ku;U3>%{%Q$w#i1Qx` z{rnSb3OcjLWi&)I`~k{PaiXL7vx&Ca1|Ksbi9?7)iKE)Tt4&v1{5I>JksYi!kBi1QNQaPxzm`rWgnErW z&p>OBYLT66_jIooKhYuoJ|>I1To)krjc6EB|N95A5KJv&7Q?UL}UGS&a zUsmOBf>%hF%@k!bLHwZ(LHPCkzm_`2!J6)22LuTR2Jye?_89lOJ*0T&ET!vA9923f zptL3hMs`R+X-+}MRq>2()knaaZgG=CuMBWJ&JX>^EA6TRYVLyDqnWNBP3~@O&L|el zNgiE+#tqG<#{Bg{ArcL%4k^TXmsQ2Y=+^eTbUOC=>PH$K~G%%rm*aJEzIVH&+ z+Nd5UySu#Zk$`McSh0-oHLdLNGv9v&P^&KllpO%pNVD zxsDm78pn3yJtHl_p`sD(`ZA^mf~GLhOz;&h*MZ&VQ9~XtY$SVyUV2oik0eWSW7K=M z@t1yaUo;ljxaTZ!(9L7|*>m(LQ`twi9$?(nRLKU@utw%rZQE?4{XE9Z zY}EhuS^yBS-orB~>eerXNOlPuMcYk{f<6~57E2cF*na{H$b5GuvWB5g`T<4fIRHQi zMe>?ge1}JZX{UQq>+w-o-t9X_>Uy+#uH$gfuG9OjX{6oZk+b_?TZHNYSOIq(XQ(-d zv%j;@PI`eGE@}nOR$IPwq{McNQX?}jiZisk#iJA*d$Jyed1;e2G)kZu-RvBS4cjb| z`DG0V`}qn#q_063jXV|o^MJnZ7s3EzzL2r2G@f78N4frR)YK=!+D*y4A0`oA7-YbW zatGvXTmXns{H}FSA_r(_Kc<;~9ga9@wHEjMl?!@j71a=Rg9el|vFF!zCq6aa`4F?~~2^nO%o%sQ<5(4;Rp6K=LykuS$djslx!-o0g zLN_YRe(ub8?Mv+%Y$>x^d&bAdLg@UgC$lYrvJx~kF$E+pZW%k(;JDaON>rAq!6cQX zp^%pXSwpo-tuGxxvbG`a*hp*$RxjcWCg>WVn3OOvrYx)i+OcCrz}F#&9G14TUtH}X=8 z;*4m!Dps)EP|o^Jz2XQ^r22|wR_xlz{^xGa!!iL5c*rZ*A^0ngNhZu{EDLoPPrNrX zJDV(V^I>v3rCUkw7I!q2W0K+RHyc-l+XtJ@cmfM7Uka`Em}C^Wp(NxY_79bdKHLd( zYy=L5?DAB*q`UM*@ivRLsl+tB8e!4q$e4=ftDfDsim7cvUmd0sqs^*V-Nwe%Ww*zKjYos=s_n8IRm?%e3N zeR<$9HA~fh=Y|6YF?@5D?gEk`aNCUKR3GOIoVEeah$eOCg!nR#C3MC&ifQVfeE|!j zEf$?B(rOm0f6g{F_KN-4Gr)y0?iZLABSU#mK-D6EVufpYb(y8#EJFcEhzXOK0+UTS+VdMbTP{`P#wFX<^^76kxESTs(w5rpRcRH9&8M9L%Oa`a{zZ)QM~0 zMuk+cFt=#5t=v`U_uYZ%nD)0!4{c>Bw-z-w{31ORSj%cH(hW-F?eZl*dCh)g%=%pj zfk(>vAglG^)Lrv4{63AO89b7@RK&2IkSo4s!0?Lo%0=83jv{>*t*J-PV!idwYo5w< z{Rmh`;^{Kc%jyUSn}ckl3dmMD`Ts5JdOlCxYl(bp~|dw%-@4aAjo*3yX{)NKH{RWWb4l^D>fA z{N?02dP3UQq+2Lu)V!dCpw&mr!1#}+@u-sU6ZxNVU**0JhJ6 z9|4!`;bzBW?NY&8K+~!+^gv|&ZOVcrlqRV)E#P-WxMaSZYCgGCQm!itNUbtW9==^$ zyG9`o-XLp;JN+XfT%oLj772A|78BLzpWtu(lC$hzrR3gh%qbM=w*Jv`!Ya-9RZ$Zn z!cu)@Ux{uf;-u>%w;_xgfMJ;PMY9ek`TC7`rY&o=3Sm>6e14}TDz62+Stp~jGJ1&{=?4Uo%AA5$KHQRFyq$7fP_@8-2O z@?CfA$(yY+)tg93VhyyNHOeRTCa%Nu`fClbfprgQ0Y4H#?-0O1$t5)ASJ1bX=p(lt zVy!KkZJrh+y7L7lYq*C;awGfNnnfd20*;q<7UYdOOU=Q(Ohc91t@J#Kg4Efbt zM1B>O#VW}f*-fO>xP8bk!|==NJ7?T4`}884Z^@l99uetRf0MzQ_=S;)i0Wi3Eo>$L z)J!OCCHxX z)4};qV+>`WI#OWO(>w>DEI#qD{}O3?bAHSoHV)4bMbCcUnnuC3@$L?=MaXbktSjP< z8ZdiRu}{QFJbTI%FXrd#5D}a}5NxNSIK7}0&SKM|5rf83O@4{?!Sj}ZnIaimG(bqV z_@wPG^) zLcNoL`i_pZ@u0+VA*jM$@J%KeCy89F+NIGLQlk!|m$VQI-xk5sX8~8}H@INkYZ^h8 zA_3^Q?p~Q;dY>BsosI8$Ul{#l!Xy!n`oK=sg{R#}j+XXd5FaLbgH9>tPj@O^l!+q9J6G1{0~IuM|rzhispgLTH>jMKOP644}`( z(arWDa(N*#>>&qxI@oVG|? z=x${*sy$J0#f{Dn^Pz4L@e5$=@owR;Z5tUG-2sj{@qq0@D_#2j0xwBI3Gb~(WvfD^hhHFm;|J+j%?z< zwBzyV=C9MRb_}wAtd?o(wemDQU(oDfaE(ES1Oh`3XNC_$t;FH5hXu35)B#^ zUv_A(d2&;H)j8bn*zehWfyJv-qkOgfeb#}o2RdgsRy~3(CmxE4g=1~_()8pfofU&{ z{lIsky`pd-%f*o@*hc7i*eL$q2J=81(^|e`XzA~j~@b}zweTXNSp=esIvgE z?L8)itOf9;RWZqV1AnQhtLydJ+$)P$w9Rt(<3NL?k+_KW77F2%V0&@l8-8FfV8}1H z1;^59FOzgwAmUhuwX%6W=kSGU{768Cpbjbo+Y^CQl&Gf~_nTev3&VbYU=A!n>&I9EA=*u?M`e?ux{4)PWi`Z=SrhMSHja6hF}hl1lGM@M0M3S0>4BDwp& zzsJ!R?5=9sDGE5bRj?L2?gOUx$J?W+IqWaiuRrmBf9a#V)Ezls_uG~djx1)#Ywvwo z+Q@(z?w{>i8+qGySl3n7)g}I>g~W_ytT_q``!zvwwm>27TjQ^!4>)?2ET?d25$Iv7 zy>F1-4~53lIVV_iVs_P__Y;K0aNrIL>C(irN=CIYa>HFHs2tD*%k=$jS_?AW-U>5U zP$@UGr3&gM;uc$k9TayaMspnZkDZg*VzE#`DJ(41p$vkTPmmD;2|72EJm<*RVoP)C zMGVPIgde@@(u%Ju8vIID-US!|B?G~>>{>q}Fz0V_1rJ92p1*c&JSYQrV5Y@o^O@(u z9cPC9-=9lf1F*tU%m-uda&D1ZmA{1LLrOBnu&}Ic)wk~e!Coq3JMD{y`_WPnz8(-j zmEiA0tg^dJ>ffr(5oZR?{+WPvgw+$KL$6h34Rhu8bCOk92nCKGzoAkk)jmXa%zb?0<7t`_GoLi*s?gm zV*#DP3D)>}Lk5EN6=Nwfg5oq56ehO#yn67>4vXN7mV3ZUbEdLD;XvM?O=n7Mw7PAi zifvK`I2{NUzC3Pv=eo>i;$`OwgJ~_?Y1@8{pX}cndgxYZ+-(Mqt+I%BmQc zHp!J~W8wn=I}!Tb{(k#XmVo6!n`#+w?`HIjAPmp%uG5$`q05iOzVWYV6zVRK6*5CE z|EhHO!2}125w}z;!*q*2bB%U+1a}{!pbef;+vyBX@RX@ndX@{xHVc&wHeMquafW~Y zuXv^$fo+8k(%2OGYMugFOxtrKD zP3ny@pk-lMi-J=EeFIGM-jj*A&*c2h_d-?3{K(}pt2=Aa^qtww$$q)1vJF6i&*nfx zXMP*be|9sH|Ku$IpPsot?f&xk3Iw95noW%sr479}sb*+yXGa5uLV7?dtRg0vt(55r z5JxgE#BeF(@=>u$Zpys$48uB`(?#}AjTsz52#pC+gi$H9zcHq?K8oo_%*`W?4khqV zp*wecps^Ot?N3-xd`Ku`e7rO@H;Q`O3pt${sT}D*CeH;5pO!bRGK2EgnAOShYj}H#ltOf7oqB1XhQtK-cJ(F zYk~gpQs^z8oNTA^!@3Y<-&!)D93IaqIE1I)+(G~>pIpK6?_$Tnj11}nk&O81R9lp# zSwjdLn&$ZE=Cwz0UEjSg70A70k5<|Rt|;rB{)+QCt%~y5%`j#eNiWOFlA!YizLN?K zrkW6B?JM@KLmLJP6FF`B)bv=TrLt5BU*ag{H2r}!LU+ejK}H)1H0ls=Nps7V=GmDn z*j#^*UPO@f6ZFpsE87^|6m`M#W-Pp_$uDr(`OtfFAE8xZd3P89e z#+}}_@c0SZm%$f7uqdbs8MAwi_A70T89CTvZOu8fwf$GDxvc%Lqitx8IvlxmnV;L4~(jd_bx}2NcDwA%(*Fv&^8%r=MLJyL} zks}+i=4tfh-&UOxCPA!!?b{_W6j89pABnJ)=s8d2%a#4zPTe8Tj^E(_%f7qhwBqS0 zE2hH#1Bj~X><0d9Zh!sn#fDsCH(NFlQbl~phmPaolQ3RV)Ml)$#<_(r%2&pZtzwl)#gtC{~ktu2%06=Vk&2w@+_JAZgEqSA|&wr3FC!naq8m>Ps%e&!yA_pGK`P-NOF9l=3d3&}f{ zwxuWbO##6fMm%=o!(kON!n)wPB>5_?l5|P>=%V69cVi;QMND2J3nZNAvqZMoX{U}; z({e|VlcSOFt8_kHOIM~S<+qIdT>lXOZe`0JYlC12CwrZU=bpyh5_?2FuIJQKMMRBZXXvn0Rm1Fp&NJ)?2^3 z0=Qr$ugx>Q1Ubr46s8UY4AQifrZN8KusRJpJ1v?(GU;&?mAK^*>5M1R5okh#iFjYe z=7#;=Jd0>Tem21s*NP zAYZsi9inM8e<;&RaFXqzp)dwlDCGv@M#-PGT1*)7;DxIjbZcD^Y{fzgYS+jlH9+Z? zodM)_E2=AKlv7$7V!Oy;3hX}Z$=yUhMqr5#kR=$7$CWgeL|Oopo*T>6HjU#y|G_LU zy*Y%-%}!3jmLo|o#Z-ANa{Q@+xQbT>i zJ|m=8h?P58FqyX=gFvleD>rADS->0`FX>21MBY2n2%Q6h{UL8{TOYzwLu#m%6cIa2 zk`WVfygokbQd$ACbb=Pro^`4O<_XoHBSo_l76ft09{(w&z0vE6CJM(wU7(c+V$tV0;I?O|q)DQh z?G6aw1NfwkZaWkG{aWInMmcRMZ$|0f!~0&4k&)GS6{FCbCO#o;_}|w}w!h_9uHphZ z=-j3LxfmtY0s(B6<@!M8QDF}t?Xs$}xp(6%#5y&I0vdAaP(BW99p@t~@AXhsbpfqk z8!-N8Fbf2+Qcsx0469S4c61O|Q^#O~(LIXamTI9v>+v~*1=rB}I4xQ7&{8V6ne1a* zRXLe0pycNWseKT%h9x2z73Bv;7&)sslBk?ek!S6KcCaHga0O4lrQ#T5a!8UPI|NAX z32`QLgUsmQl+T9Q-6Tz|2n@&Lk&$X!)*GAnGpRlUGnbMC8ig`|s+qI^H)@GbyAgd1 ze|Hyl${RH3?$Q>ZsjaPsFrNwpMhPk(2LH4AK|M&s1Oyz$zEc2G%Hubs)OC-v>rmdM zltlx<_3o66YL2)OFxK;kiYVI1DKc5t`QWFu;=b%y1~6}hSkT0-zE8sS&?BvlJ-8-F zoi>E`eiI9gbo7B$RvkbB!W);@$YXmcRn-U@m0<8Oz=)|o^4((yQHYz_a3D9~@6h29 z^I)52(X$<|OK;Mk@72Ypa85lSbODJ?N-&}35#g3ZI6CC`lVk9 zG#US%?+rn;kGTPKidp+q($D%Tm<)j5cOyv$5jRHgW zSv_aXk;vE7Gd#jwb$fmPr@gmotFl}FK?6eUBu`uS?#)@LO?`Ib@XfiG+u~;ot!JuZ3H~F=l-A_ z+yP`m4InKujc6(nsF%-x_=?Q4Og_dM6VGWYsMIE2lZQ6FHby2wz{Qq?OODn;qu}V0 z7JdKSs2jW&Is@wfQl=KBe{FwSs5Jw(1w9MCq7<_(uj?rOU8i;yA!^8b?yNpcRpyM# z*QuoZw3U@I7`m2DNA*7pnkmPgBJ8qGj!$zAR{R7R2x9o0}W^M?)2E?TwO#cv2(P zmy40T$}o)?_f+WAKMl~q=13Zo=i z*IwwZLU^oJhh1J_)Sv|lwJDOWN1@sj`5z(ezLzjC)sh7wH>#+$6TzrAMpK26{$yS4 zI3W{wn(L2%#l_5&g+wpegQKa_;W*}lg-w5QJ;>dx*j8>CSzEMv zp3?spurwStb^y(!w60{_2q`+iA8_m(K=(F@@M&m@p@Fm_0s4L1g4oXrz=o;zjUjN2n!xmZQeia&S^zT>H!M}rs| z$7sCE^clN~Zt4=^k(vUx*hpFI8XP6n;&pOWGqEjRkNkfEptiHPf zDt7}%ll}}cAjH+NL6WVhn5$G}0C8;dP}Nh2)_rr3`owsCkXrlSC(u~{g|0M{nt4Ht z4u7?~fe7H_U^%QA}pAP*)dq>`*VQmEP~@%b)C8r21LRe#!$?wJHH)vFoN;s)kX z6edT33gPe6o>@ENMbTh9-!=&-^)ZFIM)D3+gX)HXGbU~yM=@e%Oy*PlGV`*fMi=4+ zmD~5P^|}U8Z$qB;JTpx-u|1V?Z(>=z(in=}X+jz^hI!aW1BBc%kR|8zNM z{-pnD@g7ZB{lR~tWkHAv%ldRjpQ+j}W$wK`Akn^k$&OkKq}t}SPJxwo8uOP~L9#^m zV-WH*LBFpL@LrSF6xi8)n8^B>^pZTBQ8EM6M2+9BGycjDssFAgH$pgBnb#xJHM2;Jix{_!?J5=HKctM= z&-@*d=eB@@Shr&^18h9#bIF32C?a082*pqH7|B`=3E8NL1Gk3^5zvQ>RlP6*WrE|rkBLl?!)E5XKw&d1YRFU#KheE;qaNqo*j#))ISyO zQJbu-2kg=EtJ4GsbS^i%UN*yhR`Xu`hO z?<)Kes2cRaYG3gCmg!2uPp9l9JJ~edgz*P zb;$UYbZ);cwu1V)G=>V9x-bTqZMVf1w@9FRUsmG-SRiP&G~}PYGd(|}Lf5hKu8Pk` zkn^yCtm;V{Al|(1R6)Y6K%P?IFI@emXVeDf^F%@xU*UuRt4rY{>3bK!-jFNIRYPtzs2UQFVFS^7MPM z;W)bI=jRR}@b3U^CvM6)vpZi@94ZaqlmX2Uam@!~UGqw%_(^lu#=K(#5ww%2sKMd+ z)TLb5@U8py4{3mHbawi-5#dTD2!N2M$*E&3nv3hUaZo8PzT%kY0= z;f6CJwHVq;IWCGgPCz%yfE|E#g-Oe}-4J_PH+>@oh|zjB7fK(gpXr^0#)8n`_(++F z(wWei6*~^iAq7#AiqgMNQ={2Oalr&#a}D>WExV6N*UxXNj9xM=YS3F3d0Z_*-#3lQ zK9OQqbuBUx!zFBXR9RmtSK;n8Eb*Z*sgSvQIN!>w=)83rtK@lvx~Lx*YI=PEYkZn= zNMnG&RBsa}GYeuMWqol-&@`O`z8_FRc8}Eq)zP`f^hSlOrUebEj(#jti7!YaIOmPG)R~#ghkR5EjX{_ z2i9s9I7#Scv(!=ScGWu0R5}2SlpU~KWT#BGK`gE7Z>$5($mc+8u#8?d%x(w|nQ20b zj8mY#eOpn*Dv*Jpuwg9EFOY|9L0+-zrw^+6?>04bs{y(o;{;;wN$I`^iP}wpz?Zz< z15#=G*s3Z7SEPzKplQY=Fwd`2G3aufdV4O?{8UUD4W-B&9TRrJ>mEF1cI z1If(6mb<(gMn5kf2c^!XkJJz&peS$y5Hj`qS2GOdXvz1N!2Gy`T)?(Og{NJo%3Vl- zD|#-gB1-0&fY&ZUf?kbS>P33_IGBX}{nMv!M%4~HRmED|(EmGB36c~@n6d-3itN`w zr(ak}l-Sr6$kA_pgYz%TwihXaPvR2rp(`uZb0686%qv4F1%`y?6$Ujj_u!$}P_n4$ z^~{dSrb2;*o8}|x1onElW9K#XLva)PXde`kfF^9^-adkWs*a<5QtkCxq>Yn|@``nq zko<`UzV`bbormV2BSuG>q)c86moqomr3Gttb76eJ3SY#!to>=xOh!cYS}(IrJa2CF zH1*AG`7=xO;W7Vwyt7~JzGXlMP!B-b)yi^J8*LU^<8{uxkA6!xxGag?r-PNjgXBde zl1LP6X*7Eeea@g;0HYIO5V22V47nFOfCk}2_6j~lK86mtTU#ecT8=~$Ph&_!F)x`J zLWo6dD9;!4q0h2L5sp(D0bRu!-~TK1U~AQwuv_M6;j32PTDIm>>+SYu0=l>J90Qx>SMi9C9)6%d{!@j1pc#0R*7?qWKjVXz$lo;jDD(V4(ze zS@4p9Ul1vKdwWY4mjg`jyK|~c|K;HuWEGmX26rB}+8ktr1cCEkzI#m~T~QIeQc_Yn z0Sfc+fByV=3$~t9mBainmRKU%1aEKew_s*e3!)}C(=S@@(X2@ng^6|Itj=FO6PzCl zsmP+sy%a;2B=tap!~ZQ3!pN_J1X&WmjBApuZ4Sb|Cb3<*+VgKjllp}wsm!>dSi~H} za!mz;caGQRUTD;LMXhWqfn>-_ja)SkzjPyx5T#^ui|nlnwhVJEZm)oTu2+lKv1JPw z>v|ur$}3oV*t9}}=zPIQVgrDBIDcXxUW!q%9+1PJp+gD#lWxE0=e=hlBcX0`dC&YM zfa7MsFz8+u!Dxiy#GL7$htpLJ_xF1MXpkQSfUc~2(~=Kmf|xQsK5;3=Q%D>U?;R3- zNFV}nA4UI@S!b7*O3)ux@Af!2Ig^Bti+&7_jj?80fj?${g))dij8@h9lw+5`Ce&~a zC%C_zKzdWW1@NGvFhu1qoZPwF(#5-rTa}UMd_q2!v(&{Oeb|wLl-d!(Z%JRVGThm? zqeJr5i7t{A8Ss=?5sWTP)GO0;b22{u_SF9{nmQelVmvmYeT(6XMZa-H-7D(UNf-TS z1J>d&#WC1i2K$ER0VnewMEb&dhvklT7u=B0no18p4U-iKmXPZ0ckHU@?-yl>k3(;V zL9}9+7$3iI`t{`+et%c?ng{Q{ho#E`34XnDePiSN@Mpnyd!p2DIALTiO-IYfqxnfx zid;4Va?;^j7T6R*4#~hgoY|eSJeGkL$FBpxdRnwVJ6Vg!vCs(4d=}Asa#{Vd7w~4C*+kazXQz7x{j}0f!3Rg{0W0 z;^N{#0BcLVc&rRz@(^OXhumkLu?Wl1lkS@4FW4OKd@U?3zoOuM%MuYNqdQ1%4k)P% z+0L4;vHEUaTU%?!#=!v_3$TB@GXHmT3Zgz#>Kj4km{?g~*^UJ~7&k3J@sJaOWyYQ? zO0!4g*?vGk%!NvjHp-Sj#oTw8wnj2ZH9|F0R)%dIu-1P83u?IzKp{E-h^$26rFVqH z4KO<7^iTv+NzLT&#PMOZQ%Kb=G~lE=?E^kHrWTvU)1z!QxJepw5c5A&^AQL>Vo-KBs z1llUC1Q>=yEwVkb<5#IOxSolTA8MhrdvYMC_+Q0(TsATipzHM{CKBw38B9od)&U~G z0O;2T<0Nh(h$tVO_Rg#~(dJ#X1FzB}G?HFLiI?GY`H4M;dM% zpUff+!Km4#O77O8OfKov2GJ!@4#^n6XMEvT2No#mKs73EwUh4OXX1*jp{GaE4oDyh zV47(Bfjex__H(k~cBKt#Tt9942TKCER4w^DULuz!r-jBRQ8Fdu>j1bS3mKX+CA#H| z;W25J^7sy>7yUr%TT8?blqdrf?mgIMw~+d$Jioe0wt_6lvuTuSh$$?Mi#)_Mx~{oNmA!m1JHH4R5{CCy`L#Em@u zjLWm8DsGTdGgD5!mWo5HG{+>)*l}!HmeXh4WwgwfJyCcNODeJGnEQXXY&qd0h%A6}hdy8RzQrD&U-`O>-TmQiy zE${w}3u$_@Nl~Npq3(qPx$p!qqk_iJ5?yP2meLG!``Jc2U8XSR9Z@0Xqw&t}gL(va z8)WOLB0XeEXoNq+|YiY!+0IhP4*dPuOU$l3>?PpxGRx55(KX8K0- zoQJ@(sipqSN;fG6CT0?(ZL<^O-(#a09oVhLX-8QO8qePm_>Ag}c0YqKmXD6gp7x+s ze2~N6uB{!tMo&j64Gy4%5zlDYt+*X$)FMB!5l?`r9Gy&AUS8fjv5^Oy7H04K-}~Ev zpk|OO#lE>LwI)pM0K9L?z`#I`%z`n|t%r9a@Swj0=zBqgS1qy*aqY@LMlziv3%tZ?><+t;(BA~ zTGR_`P`MxU^=_Hc0=tlzaVHl?lE!Ja%^!BSF%d_5W1yND1W=3nT5y;2qoSiJ1?>C& zub6o}RsnDM9Unbe0a?U%*iJx>zJ$ME(Lx-d-8#5{vnUn`tyzwUir0>Bd zUl;qfvlo*?hmw{F<3=NXwO~ID9^T*)G!?-T!4(TAeS3JU@-_10T7%)kh^7eh7Keav zcjvgWPWJ)oRKcKo)bXFY&(j(`VM^BbzCgx1^6t;lttJCZ3JprhDs~Eul(w1n&?Wq; z;Zw>D1=TZ4Cif_{@3H{HADR!R^*a;>kjl^jzj1MnX(fV|yb)QZ#EE&jmd=abqSqproLNNN zpkdSeMUSC`MdJ#GLNJ2JsQNlk3dXJ*nUqia9jOm9nFXMNlYl#A9v_f05w-YAn2cQ9 z+Rkiu`Z-yUtr4m!>nRr#a)qQ-4Co3}@#6RxPYYwNP z3;MAXLdyQYHn85`;(npsmlR({gJ1W&wIWJ9=&l^hU-|)HF`2*o2N!&{QcW12g3rv$ zss^%MGbFpW7R1%SM!^z?ud|xXYScB(VX?V}>Z#N0ANgn!abq2wcjBBC|0egf*C7Bb z(gy_iHUU${9h6_2ddyPlpe zx{Yau^AipC|CA|KuO>H`O0TkEYLfcW726CD5<4fU+1au?0JP{6kBoI171&K{O0d?> zq)#0z{?kq~CNh%ymFc8;Tx`CSKhb5SG7J_HsXJ*?d{$dQKzO06{+Gy>m;xIOHSly4 zjUyfwWz8i#cC+|Bxh|R_TuWQp28lU-i(*+_GC@rUXetdFFSNRIR-l(-v*G-_t}5>T zY%wiFl%PSEuVMk9(Q_mZZQgET<{)?75?{N{Ryjx#9`DehnSTI}|tj0OmyeKwk>F6*47w7kCXcZP*#sH5kkYFr%cZ7(|X83~{ zzr|RDXO>XBvF6ugHZ~HPQ`C#-bXT@SP7Z$M>Bx@>$LPnc@*|#7HTl1ILfVYl)~8aT z{kX(IG+TpKa(Fo`SsAKT9@yI|aqTlW zN-6ekwifhBk`Xv%ey?o*eH<_7_*IgVlTDgHB>y}yF@gKJu)f$*n>Zs1P3kxj$H8S8 z$E#a)g%QDMWbpt`oE0rp0K?AaS#LAY;$ok@0se)Fk8oX+I$P|}n&8P+5Z#3I8+F3s zKiq5pOrW*4+~3sNPvqZYhRKZ}k~)B4uHD44GcBJBRivo3w2yN7ywK+%DpCdnVp?-nKueeE9b}FBi)n}ir|^*B08;7bS+nlZv@dv zLfrRa?`#RxQ+NVLH01E>KhY<+3k|~vo~jLuHf}e8Vt_H{UmgIe&f=1QvB_8SCre+U z_l1=NRljKx4(Oj4;fmohj{4Dk7**1B865MdOb~f@4J4OrTwVFt^I(G<2F7sw2-f-# z2t7WRtrY;5^GJii!nG?79z*R*p_Gccp08&bs+kZ+!D`FAZ0X~y8$5G_#ZIaLwCZq{-5)Z zg4nkSa3)E5Rc6^@*8TA>X}o^J4~`~Yl3Sa{677&yw*?w5GLWcu8|Qu^&v;j+L>8E+&4 zyps%nv4 zFAaJe+GlY@f}w|;cXOk)Z?8S*uyND)W-n2qS#Ly3Jvhsh! zW3c90h@Kq;vZ4lDOeiuE5{{pUQBt(+Lz6yXaED_Ai&5PSjpO4Wwm@tMT$WA>C1{tC z7qU1eA=JScC7`L8RKkWFYm@+Kf7axRQ1v%}e>0J*{!yDUk~Ak5hFD%>kXckz#4jmn zUJ1n3IxglNYr!%v89!VoJ1eXF5H((V);A|M5<>K%3XRK`np-~9!XM&*er9b2SuSm_ zd|P+qTkzuJ55A*yfY?!Z4feq)9vlgNpa1vmI5XBWJSu4>W@bKn1mm(o4rTm_ql}5h ztwqn~qx19gm5T7$sOE_al~s^6tn#YEO_k?9A7u=h$Q)BwP@^Vu(xLwHJ5|GIeDV7R zyynQ+oAViSM<&eZ;~9prz0F+6M``y)*R>Zd9{WbM0hUj2s6^tYx%v&ub<3?vu!$3p zG-zclJzm0di zzwNiZ1Y>N&MBb?^9U+2FgVhF=}0GITJk*nI`KvFW#E3A+JU=HK4}7h(BzXx zRRQ)aqw%hyhlj^EV7a4JKe6r-_&;&vY6#y5ii%6)l$DuzyA7^|N@j6}KMzRcidTG7T9K${E%UAVqwJU(-e74Y)5lOqZ)tpj3akRK z{sz!;u>(5z@*o!{@0n{%gLy?Ld0r=Pjr+l(sG*+Ti#fW1Qna`;Xcn7UNaK0@Q79HzI3q_z78A| z2&9bq~Z1|p6q6SzM`fYEy=9kL<2s%v=5`=bGGhuylk zYgHIvqIP^DchOe~mI!q%#AJq3mT(>AD~e{D4U7b^lPCs1iSga|Xk|amF2M=qiDbSj zt**pjfF--9iG{z|`5rUlI{aX__(@C(Ok8p%rtLRDwf>hBo3dKrTK#{jbgUQhTAa1@ z_4NY)8tK~sSaz7>6B7YDKMPPx^(~08FmV}AFyux1;qa7{yF#~2JZb|%eyTHbgy|+J z_ZiI*GsyIBA~V1hBBLl`#YGJz^$_niSS85w-@tBA4uL9^Z-4Ql8eF0-{??<&_#i+s z|CC}O<%+J(fvzorBWA6ngpO}MC9Poy>`K>522bIoUO*|CTRAPcrIlB+`;Rfk2IV5o44lLYuV)w9|P&(Q{t_a_qUN-_;ud9Ftf7CJYp?!%#_1 zNs&KXYP$qnz0~t`ew)GZm7cJNX{apf;HXvRM^TdN%VRM|5o%mwX5N6p12AN+)=B5`kxfwX=mvgmdMF0(1HDm|BBhGr|Jz z3jf~U4DIbXwfIbNw7lI=exK*I1m4y<7~EKc96PsY;dx3;%41gU&hZ3Ky&951&4X|u zz32R^HeE;an_F17nlWpkpY~~RZiR7E>Jy{JJSTzSq73cwl(%k`6)9ph?q|C}@O=c8 zb(b18DL;mUp)!FZ!8A|amx`M7ee7|g)M z%#6&mw1G~@vObzCDXC_R`vjzW%INk!PqP0G;2t9%EMZa%9B^u+-mR@INj?C=Mg|mu zzb`YNISe^tW_5?E!>7Wr(yexSBE48| zRC;9H>{gb)kuL*Ubh;Vhl##a4?8cEbj0>zEqGQIZptXoTXo6#{T`eE7WVshWP#+rw z5RTFL21nYZrq||f?@+93qy8uK$IQz*g=R~6)mcdjB9g{mqSSvmr?c_K%sTCWbWChh`^RNDT$4BH5>X=%MT0Ok{s}{d&}l!&}2DV#2MTW>d`0k9RP}zss$XG0<7d^TKoixLukjf3=-12Ovg0# z$#i2$Mxie4t^8svM*9BjLiE4l)N?@GlNRog1cZuANx(=t18IE86V!qa-88?S!T&o~ z27Nofly{I*?&|95w?~fnBj>&&BR&0Eh|9LLwDja=-Fm4zJl>1Anwk{&d3Z~uN_ELI z$rzB8`X78v*o8E)>rizPT<(feX5~z_9s8L`qoaqLe8Vr| zE9@CQm8iUO8{TkTcucoZw+W@LHAgN+Z>GBCR0Ss_g&K5)w4grz)oeYG_!F!)9^_cZ z*1pFn)WH5<557}`s76;y>#eD#=1)`5A(1_LHye_ZlZWR`2N7eYmanf{*B-E!(N8LK zj15}ZM<@O?TUD5AOBVkbx$~Wvt$tyAbz8Rj6Vi`pxC;e+A@{fCrUdHIt|Nyyd;@W;WAkGR@5-LF*-Q=qvKOj~_o4(dgE^)c5th zJqPP8&s^ZGc|KmGGP1U|R@PI3UxEM!2WP94Xst=spkHP7-oeZ3DkU&5urVhm=KvrC z9_Huf42{@%D_5r%7fmostPNEs6sCJ%^dp7oW}~E+`npRa+&7ofg^PFa>DT;UY@d>i zjo@<)s_mY;)6TtY3EwnZIj$;`8f~XAcj=%rbE!?y=1~e?sJn_8HyIu*!Qc5hw$pk zE69i<1FKrKqzWZUI@rl$Dlw6eKMVag<99Hqv0xGH3%#pRU*Z*IcN zL?zyhebsV_eQ|}EN&K`-(|<3VxKlqx|Ik>gy|6aQoa@ifri5`j4+_;5q`h+4tO_C{7Bm)6PA@s=?)258Q82wJ8B$sxx?kn7T3 zm}qjNRIUE0jmaUiZZU2&09wzc=&)p8g(oTs&il3Ff16J%)r{t!y6I2^FV|iB9Z2~2 z+&7JXP2Z|grkv!t&)nrfO&%;GP(NHwd0%N}B<_gZBf?tbjkz_V2aTE20!szhZ~zzO zh~C*yj;4yr&|x?Z_04sE0^{=K#fAUW^76ya*;xa(Jq00AQA64s&y*%w@NKTll>}N+ z)z#F5RMpibM8(AB1w}-zY-7osBqNi5A&e4*edYQJoh?k0xkFA z??IfE!*Ss%3x{iWJLyV{;^v484`SC71XB&qz7``&fN)?{&{kFn) zy9?^HGH&yZyWXp-tNH5pwoXoap#~RZja9L+u^luQ?f8wa!w(Nb_NS}gOYYtBIyHsw ze}~kEjX6Wwd_gD};_V=7d0zW$X@(+BDehyyskBe5=eS!3c>v?z>Hhxyp%qVatlV6+ zs*T9mS6n8JvSjY|ze~m1!{u+39zW3k|MUNmCvYTnONjP;*jf0vMdI-zDaotLRmxZd F{Xc1qXJ5otO5)lp;4g>@QQCdn&1q1}59s~rm9R>_|WQ{sf1q76rTUtz5 z%>(q}4=5}MF$g%*o92_p019b)%gRYQ@GKM&;0p-^2uM4`Kd(YCp#M1*g7Tja{?88m z>v#Syzw>|g%m16R{-0j#U)TSCb!Whf84~zHgTRo0kY{H1G;KNPz!u%S^$q_Ds4!%J ziYjDr&VXZJJ@D&Fo}+yoF}r%9uQ3;1{;?+L!dI*wr&*`Z?|FAH5Dfh{H^%$#)pvS& z-7dF}Ea%6yB?QKPDXp+ISyYFkXMw^uD{W>U4=h%Vsp16?)hKn+2`+CEem>7J1FBej1 z!Q+RDg@|RmnT?H&*VjwQctU}wZjW1$UKx@Q6tocF_5JLW*tv#p;tL&bQfq*tx|*Jm zk?dh6hufL<=iYE6nPhxSOdNhpL1~iOP{4F^mw4Bsk-?}R=K5MEfqug_Zo`0MV^6M{ z4q(B8^@*}|Wy?mhP&$rMPv#G@1WH0U5U`2p`M-a^vwi0>#@6GOzzNwif2UYbW2Gd zJ2?%QAWH`=IqA&Wp=aESx1ggDK7N;TIzp@-t@j3H3O58^qiPM2%aaxAvWMr}eT!lZ za-URc?GRE6!ljRkRaOSc-%-GO4}mB|5s9dSAB@i!?Q*|HJ&ZW(^0*}hO$ZSqeNl2c zx)wQZ0h`unTD5M|eRQqZSidB7*VP-@k(Jddz_a5-AAbtd57~{}uUP?UO{buCFRs^Y zW$wu^BHQ_Qf`RG@K)X`slkhT5!T+6+Fk+0FfK5247;QvqnDJFjK}8|F+5vs zvZ&MR^E+Rzd3N)0jYI z(HAj8`Xzehn7E6Cwf38h=GSR9*23YyK} z9q?a?!HWL}F1M0nK6gTE3uTJRFeea4Q{9fcU~9-_L?U2%$_{L2psk2v5{|FJR19(A z96T!qGaLphYhF_+m~gyASCN{&US-jOA4A)QWyg4PXY1n_8Iu&j=1t0+xR2(=fq4g> z;v$NP5t_BS+CP4fo;=*{k0C&PBCR~@q5g*`Sxd?FilrV#ezJu3Ci4p3qsX9v2kSlF z?)jmaYumQ3zFS{vd@aj_#6WR$>XL0QlM>qKn_rD|Xdb^$IB`BI;sd}%2MCL1K z0M2{&aHjGnyFn{#WrbgpN`;kaEk;6`gEg1PwBt|tv`bk0F?Xbf9lG1w0aciD;eu8L z75_uNjcP%^hZX&~nG(jac*UD{?6ybClpl7`88Xa;>R&9ly)8@5X-<)eOs8lXw5*qq z25jZtY19B=4%I=2z(gsEXH_^WwSYl>|xzgDA&tpXKS30u{|i; zSrNrcUMc~nZiBkKI}3mtQ6&T2pl3Sp?7rNQrcxihh)$WdWGLGyDO2_jMm2+#)LFM} zBk@}iZRArt0%4*(5%S+DI5r*xf?w=6@k9S)&wu9)Yb5NWgl_M@49tED3ky@Mk!L%U zdFy|Fu*+1Rh^(7^viSTsS~MP*YQU?8HV`}3vgc$So_;>PC_ZPsFb$zscj zaPc5~*n(X%JuMCEC2&%;(KKVwElmk}gkR|g%G$OT7Z)>;`(Z*svmo7r0Bw*5lL2N5<07Fcdk|B0a7Rdj9fr&E(=h;*# zz!FwpH?9uh4|?1AMoBRDs-Y?`h;+T}ekN(f?=@U4^U~ax43?WC{Hh=819RZ10j}UD zaCb*S5G}z}muE{=VzpaYxjkPypnHMZa#998UXf8A=kN1-@Apq$c)R2)5Q6bUWC29Q zgjiTuTzWZdIX&gRrGdM0d$tJHM0?tGVsh`vRIm)odh|G$ity%Il$`W@$gSfojB%){ zQKNAY1lZ-rvoLJmYjyC;x^KZq-oKS}{b!1lh^f~inZ&n1nA!Pb6~nxp`6Pd-zzQdj zet)qQjZ&b;Z1OUr{jS9TP9OfXQ!q623oSf;=e!B_M@A0R7hxVl2$68MO%-UTUC^=4 zWTl|6Xm9jQXT;;9qgy8K!`-h*Rn>(YH{I%71rKoYJn>SAt4|*p4*b5OhD2?78{SZs_lh;&%Ws0Kmx^XcfQ#|u>)Kr zLio(1AI%cbrcUjf?S8)2){P5R&P}l_ zZ`XZay3lD_q(j+wt>%UzX)0(pVR=o9JCiqWPWK3 z2E&r(n6o)}Ay&GW(c!A!Iui`OfhV!9Ojo=?nG&ncwlv zeJ1e~FoPzircg$qr3piTmD4D*?G*6+6t+jZuOow$FxJ6!?r@Zl-6pCJ&GI{UiW7hQQ#C{9k%P+JNK^SNx!|L zRx`>e&)D}mHgBOvLCj<4K>ofW0l{bqGB3Dj#rIksAw8JB@tX0JPLJEY zKyYXz0v;DB5Srw|w6X$NcWG!u+y{rRCx>QMSHoY3IkwsmEZVtqU-ZUR)MphMY$WqE zxtK)eq|3kO(7doVXWi8gEaPLIH0)CUod0?-lw^dwh9F|`Xr#Z1C_4`)@_T5C(XeL1 zMv#C(gbE%gpdihl3mp#?U&XmdOHb9*)dRJ24&y~O^?QHw{dI8HIcySB^MVCVPshke zM>mudyc9Yf9?$>D1^=y{4U!y!vzz~dTl;M^FxMD~ROFNumG9$WVZGH(r`PADTDvKZ z|7isj9135V;Pvmd|4tw@hxKB)E3gXt(PF*UY&8^yhf>)2G$rC!5$xQe)w^eFOwVC0RW zIAxINArt@pHgb+)4b{<%s76cj*_e`U;Oh!`8)HcX5DawNE#LkcHu~?q=ZH8B4&x=$ zcvf#rqhvbSS!r8|dv07SEHu?R{SL!G#7hS$gD6Q|FHb|O-3O^|EGa2zZ^vdA{>t~+ zgJpmQABId3c0MV|LRzLG5hnyWH~9v)OlTO<_w#U~1qpZ);TO5C%i2`AE-HVoHrJc2 z78rtB?bbOge}hnnETmY@m%z$Zi-sfpW>T=-uKmyleN-c`u4W40R8hB2$fDYK{CvCi zAB3gK@j9z88H>|vGLK!0l9-&G^}nCyygdjJ{Bu%vMkL?q>qS!gTl1B>31u_s_3u3Q z64eV6sGypG=JC@efo1-pFpD=V?Ni7(b!Bri#>_ZUvh6L+FPiq|y7{f}0YfHi5;klE zC~$qdm_OP{c&f<4xH#zPfp6-anaZQHy;RTli2_!T(mP=zvVA8>mbe|gr*AC31GqwoR7jk zfS-`DX952y9=3C;wZ!iV+vu~`Cn<+Z+vK!A3JH%X^x~MrcUE5P1w_Fb=*$8&*p$Hy zj;*Dmpo6+7EU)#+Z&Oa(`{NxPGAfme-KE06yFH-ky$|!U;mAbH_8aZ7cpSD{-JYuI zO45wp1@|w5!LKhlYY?8zg*nr#rHVuXK3w;(xA3vc8cERMoi3+)kxuA-T@E@zjOMa= zmZBA)B>8*cV2zgKlp4517K`YLd`?dnt5q5`gE6DvgQ)4J{yrr;Cc>S$3PU2Me8ctQ zG1?-L#_#(h)U*txviD#SmveJfl6UTs&@XiE4$vdF`t{3uahK~()E_^QMNGo9w!;hcdyQ$D?eI38St62$A&-!?q$_>gZs^ptrw~egR^5hYNu}=>w(;=%}g1 z3{B%%3C)FSA#r5GiHN0ILkKXb=$S&$0!^xQ+K`I3Y07fu+Z+s@VZ|Uhz=}mFLx+UG z4v}g0sKOONleNL2X9M_CuQnZVbEa|YWBv5J<*w;kBd-a5@-3@HeO2N|kY_le{AHwhe*A-GKIfaOa;Wm(;(uc%i8ktx?hFF zUuLp*H4yLKM6Qzv%QO8nH#J3YI$3R^vX4@NN99)8AX|OBifd=_;($RhT_t0+4Vfb3 z_kQ9zuWlNP`@oXZVzrpb8h2Q(=5{`^-Dqa!Op_%8kT9LeRjrz12Twra(gjd0jjstt z<}E8iDo@||VClU%q=@BKIiUH08Qk>#Gy`2y@;_+V1RF^)DPVxO{83czeQZy zVcZ$gaobZIlzvia=i;XyX#D4dPgy15S$DEKp}j1po7X3Gmtrq)!|Cm^+U~h)`>WS) zS(W*}=<_SQ5h5*S;*JXRHWCIEU8$rjFlH2goEs8|4{L}CfQP54068a}6C?7vA*tr9 zZ~Wb^eEpdplcar&c&)l20IS2|`}(jS$BX1Hxe|2Y7mH7frgdY8aRWymh=P!j7k>~5 zvO~U7(dl-%X2A&)&0;>4(cye-ys>@~U}Hv)nao~SM9X%>s1Vdoj!nu3(0&AA&|lAy z0hgapjj9UOQaUl3+B8zRzl^MRS)KQPN%!ZzpJIJ`I$bYQbja!fN?s@ug2nUvC8JRR zaDpkysAa0Uik@-!;Eqy|t;)};a3Q23^LhTCp56Sr;Z_vkuV})8A@~Bf!{|QHg>*Lp zD8kSxRKeDbV*~nUwIGZK(|htmy&q>)b$1;@8HN7?w6M+_p(z)L)xPr?-iGTD$tuVf zY_=#+C`6@$2EXjlcOSe5V{gL* zjKFfEHQwigHJds@T13VOU8OX06;Pb2RcY1%@uqU<6&Vbj?=wL&rDMvDK$KG&80VE>Mn7SC8|^UQvm3t%=+INpB&)fh&S7>Px)9D~vbs5u`0D z0yTV&w36uc+7R*;H$P(tg{qb#71 z@SE+avQ1ht&6Xnhl@iHZ_#GILge)lV+U@iUvzkx!`#NoPzv#5uMjnpHM2#qzjz(rb z^L0gYCa%$jX#P265~t#>AtdI}c{^WLOD1I`|K1T{A`IJ~9;61&r~ETT8I+%3<+lUL zS@j>m^}u@0<2-NsV+OqguZe}fP3WlaSb=Fs1o(?MC`<$kh!OUc5$rdj0G+IyOd8Ce z05M5=Z}%-HXwLL}Ea*1$<8Y)o%<{wFUGV}4@|>cESd*K!m6HF)LWFe$i#}_tniec% z*x0chyVeUlKlH}NYdye-eksK0zQSY8B zM&U*`MiKE=5vYQ;j(s0b3tsL;pV=J;+!k@+!>kdafmDh%ulWGTza(wQ_ zJo( z_fT4I7Psmq-7wKZq{=5Kf6;3A;o!MKonaA0NH{lL z$aanD7nx7w!ns2Mu?{_tm`B8vpnE%GVk{%LMkvV0VwUIq^@Pq!LlO5>H2dD>ak=iF zo*BN5Z{!dZ%jC>(o?KqusWuQK2GS6rbFu00J){6U{G=p7{}4MwYPn8vT7)KO$pn#S z(la=AHAg=`B_{a^#>44f0!5@}Ak-=p$|>hnd5kleKtknlxUNN@4l(w<|D;SY{$_hn zKrQS-P_%U8D~!>Q8Qe=>FdUtsRf=6ib3B4&Fx(DZSM}E_+)+V)9!rlJ_bTLS;?PGn zfznFo^61SC{aGtH7yUTxMoy!~@RxV*Vg_Bszv2R{zWW%ME=YZDFNRyhha}IuwrZ|0vEOBo}-y6CXzlb6H7f3M*2%# zSSm8OU=tfu5A%z}@>hxHAZ`)-Y>oGbQpvUgS5uK32=2YHT z6y?N4T54{O@dzBx_`!HGGnR>X$-T?T@5EU+CefY!tjfLtgj4U6Im6J>?1qd{;GRay zNThb|k?=H~0y=Zm8(1zVuiJ*V)AZh%Rd+1R4Wfb8`NWxoiM4pX9BO8;(o5R^8#7GU ze!KNcWc4P7y<{ws=H?O_8ko-Sq!FNY&z`9etrJrx$#zi37N#sHW`VX-WGEa*luN~) zA@eto6bNJu6uUo%!PNT|t2vqYVMt6F&mhhh){Iz(U0Chk+ky2q>KS?$MX{lS zg418pliK$0{_!#`iL?!&0VS0`7{%|2E8lMrKDjaq5>C_uC9H*T_3?-`=aMB{U_RZ* z#Jy{X#Z!^`iA2bqiQyu#?!0@O>$8@&^sLEEZ@(c{rWYWc0rDZB1&tJmcs+Pe_(G6K zM-gOk_l(zSbU2xXcED#RJR+QFD&&4!M)=Ak;GIxx>(&pVZk$}Z(#4t{>*C93zY%7W zOeJ{D50Q8*cAWW+p}oqoiI=^Fs{iCU+>w!ipu~Lm+5$2w1_c)ZYKRfJI`C=jyl&fL zAr=9jwRN<@(4IalCO04-x)S9RgiMxGW-DUq$4XuA6D>1`q5UIvTz*B2m^(Qs465+1 zE7uY8Go|_gGDK*|t&t&H-+6{foY52UsHky&wLxFrStMxEK^JKK6)RP^nqfBnfh;J} zG9q`yjOG%U-KI@qvab4MlCe?R19($vpO?aS#fs_OtjQ>fgS%$cWfKU>bOzn#h!OGH z?mHPu^gV;hc+{V=_RG!MHsKsK>(m|iA_)yRyy4F}{`GTpa z?%V5|nP$>{K?+y(bGjzrC^AtCI^UkKP5s`Cp4$7V(P-_myT-+Rtq^S+UqItL7&|Rapu%t;i3uFcxre+i6 zcTz?=#-7aDW$YC3VyP&)ow715x(b10rbAB8wNXk%CKr zoY<7;;Atj~0^`BTIB@3~;f$)?cyxZ&N9&_2kQ2}G`wQ1}zd}<3iFsG6>aQ|NFfwUQ zKar2)3@rX8W%e!)KYIhr`FHWfrdhr>H~!K30^q<7fdmd) zqpt_nS*EJDO$UEY0gkYa$4c>F?LR&q(H`FtWP%puP~E5J5>d-#8GBMh$!_4y&bT9D zt#>jt-HpT|Lwvnp7Vt?EEUb}GjRxtvA2nT- zTI#qnt~f)GQIhFUBvEg{6LGQvl+;|}M`)tW0uxeL0?oAQQN@$##PSjt=S5(3zHy71 zx{qimf8({#s8TdXid8AIRmE_W%(^z0?9;Y`8F@^^r^`Nz%#QuWN*mwn!p2M9B^5=T zj*6IMTo^~E7HA$b-1veWU$(bCHN$4`d6&-oexY4i$82Dglk;LPdcQfkjzhFa7SD$U z>;Jw_q}^}OtY}3?1uc7%v)l4VKiCZLY{pOY_`BI^r1`q}I?t+SL(thDM1J-rganVv zZmav$|DOPk5YtmrQ+sp;G(GzX@^-mx{#b^U$fi2U*fK&9L4i{m7RQcag&!`%7?W*1 z?YoPj=$W`8isvxwi$sEL$3fca+taI76 z-HT5%kh88IYMqD+xrkX{!#2gNKc}*w%!e{(cve#oTg}nMlL)E0uEu!=+q{r`RZDkh z5w^&Z*sVr0Vg4%Dm6sw%tY8uEuDMy0c4&{ID)h$_FIBp*1rPs>3g3N5O_wCWm)c#f zM9*Va9JsLoDDYkZ{^ry*w|%EL;H|sA)fQO{e6Eh#zwchR@3LE|-<^{rI;_k2LWJ^8 zZgg5iLUrF}`GkLTiMXolyUKE=qKc1*y$QbSkm zo`fx5Nm)eP(*hIAF$t2x4C0V=_%{VM%^&StP8%mkiTpzh{V2(vQhH*DY|G7=FjVRO z8YkD@^7(|LUUaHOn>Hq%R@I2qISLf+;(}k0Ydt5GKG}=Tg>71e#YszjSEm;abi{-7 zHo#`COVxcB&DHa0RuV7mG6uu+7?D5UdfFW_3|+`d`1Hi?+ha?*FEyuCwOHKXwslnv z^~{W7hb>pR>=QUL6Ud9ap%AKd_uI@9Z_Uypq9%Dw0kOpOH0Soc_bGBH* zjtI`W)H2x|+g1wGSBq4WXih27*v4UE=}k}{xFW}Crh4?WctysrZHr5ta7EfmtJQKE zEB&xR+(;hKKYTIv)^oGGI$CEax!5tB2QCJ?N&5MyMm90lzI9Z)s*DGWm?F|pw=~9u z)i7>wGPdsoef(X&=G}f__?$(EdZ00DA}3@YHeM9ybo6_yGD!S-T6Z|c9~|hB_Qbsz zr^@d>EzQ;%#nje?d;^DaI)2 z?@loCT>#NNJ3UVx)}RM&{!qr&>rD`aK)}9K9jES}rB2s#a|;X4SL!y*cgDKV;6oUW zDXzyw)zyd48iVf5B3qH~@jLJneAoh>Oklu`Q$}GSiTTuXIjS*8QGGLl{9A~zu_LU# zkQr=KMDj!9a9FXsTYRBqW0URCET1-{nyf*^9Afm(am%1=^0*8d{%pWd;QVUx>E;q9XRhSa+8aSJoWOGo2yZqDNO{15#BO>1Lf>THL`R5e7$V z9Y?jY@)o;K|3uCsHvfSfkNI)$tB&J%|GQ*G|5w)Q{Gqpp;|QeM1R{3B?;m*E&!YoC zV!s@wH(3fkALXz4g{g`vM>=ux@XWuP%X!d0r@_ae7~_Z-V(ta^Cy~7+l3w~d{1d|e z6~m_R7Mo>b=wIFmQDklZq8)w+oT+@ zF;m-Ah$tl8xdljBqt!W~h{?NB_f$KFkk_1Zss< zKO(kIGmdC`bYFkI-6A{qA%x+??8n~eiT;`fb?r$B!m#L~$iKSKR=~7=2~=t+@=6|WNKCbSs{^aWB$AOhOw&v8lYYY!j68>by)gQ3Ii5l-c03nF zd#R7yh^b?&t6;?j{c`1Z8K>ZDi}Cl+{{GKG2xTNJEIe?2LVTt!Y}>JQ!<1ZaKH8cB zixZ+G%4-WVS8%4z7e7oMCoiyy31+YmdIo}Twyk`yGdjvpz+)S$bRL+~_tg8hS5p?h z8oCkwtG4UHYhId)9hr0HCA-jtAY(x-R!I$x&apB{j2e;Uj(6!~5`{!XEhrJFVH6o@Wl%mbB)J%t#DR-|ZUyP<~sr4r(v zETOGg0~AD|ys3*(8N6|4b!}p$$CIZ9Xyez2`WC)0q1ZBL#NJ`mb*=FwLrlakO4S&^ z_KMcpK&YVh)2912(Z`A8vmU3#c5tsK4X_j6o;vt7h}9dgz+N^k2<#(W5G8}V73!!s zl}}x^ZO+i(UIel7@x`!MV_n;ZuJ1TN5exQ7@#O*%%SPz`e<9o>o>(=e)2zicUUMB+ zsvG+uq8Iqta&#Cm;1f$BmIiSvZ8AC3b=3(^)gYWFY|Nh!jHo^Qyoc2XFoslB%UR88 zA{96Q2hXf*6nW0Q_OaOhc;f$vSx^?5q-=2^gJHWB$Vc!ABI-C!O3DW?ly{-&&3neS zPW>h(r<%oKRUEWE|5h^<#WMo2*_1&)3e9EUqIhJ>zG}9i-swX2aNdFZ@}Ns;A89rl z?*$rZw>jlr=C*~a8rH-^%W`hTyxu^4=?70L;|X&V03mIWxY&0kwUi_<%zd2W4Ae}V za`@8Q$h8`+hP}OLZN_d(OH1rv6hzVE)=urYb!v_8#uW532)l|D*rtRKJBhF+1}K#~TVw>AYbDRM_Y+W8 zYntuqFw{NzNo7XrB1sO24Ysb#sLD)R!(A1+Dp7Up-%) zeXD-tv#D{n;P>XYso|RiY7k>pY3fxPt2DU*lJUpNb0zH&e8IL$lMT#u`aNg-Mt~yY z&vkD9PrkK#zP2xwdb*VYzB^aFbhB}}{mm}HvDLwdh=>yu2{AFT_qwJa`k9K#jz)S< zb!2PenCmh8&lv13{aCrI(qb={(|L+kiVIYNO7tl%%IqfUR@>FVi;znPBd5YOa;Wfx z*gMpWthFH~7d{Vyw<3RbZ_}d)F?!Iv44E2I`OTnuIjPE_< zpc{&{$^g|KFLFGDxal)}VF{cilGI6<0rkBh8@_Jy1;6{+%f zul+H+I)gqxB2?fY_$N*jBzbgz9|KJmvuWbWY0mfe#UMw==TPFxrrM)buc4uk8W25i zinAw55`iIMgj|mO@BLp6hExFm`D)~5c`PN)$E@w|5Cw6J%+-W((VM zg%s?GayAgy^?dA>#Iyt%VgE&y-9@R&!32VX(DH0W5OyW`BhggY6fH%7uKPDACPm0a zySQ;Z^BVnfaDLrCadyTN9&>D_n-%JPqZQ)iHX~1=qLpX$1%H$CQ&5i+w8eDmcOBdP zzdjjzR~l_sSqM&oqAdkToWoFjMur7Qpm-XK462`}v)gX28tLhxM3B=9^vl<$!xb~I zAaP(x5%cYF{~HCEt?hjP(5cx$9o-GE>$fX^mJ`~CJGu}V=qv{6aBIxV{QbInA8&tY z9D$wkR(a}s1X~0X?;ja-0P194dbg`J&x_}V%E~F&YNj|r=$pq_ib`da8nD!bbetB1 zaz0l2r4e+rg=v8fkKW7J%JdNHc{2CIO&o1?+3Gkt{YPuG=B|x;{rK}qHQ`OWw~;!Y zBwDPZv(0lm)!bUUIlCScT%u}EkYngzoH#b!ub1QQ0p_;fe4_A|VLNt6;W$5OrykG$ zI8D9EV<+7L8RX&anOA1@;}1KOT4A0+hV~1=pY!0`qnzu6B_2X z)}KaOqu1p&7Dr$}BoL#LO%#xE-jB_oNP}T|7=gDwxxK9BCca2*q=uyDHidC|fKXs9 z6vP!`VhfT=izn7{a828XGiawJe*$>=tKkIGhZl@L*<}h~=z~Kl-aMqpocOe(kyt95 ztWa(^a2NeE-E$-{K-BMO9cK$}nSv5F`FQ4~n`wjS z_AqCe*0S|4?|j?M90<_E_X;<;ZOx;Qz-Td_#tn<%3cRqa`>nLjl0p%b!wi5$9~yAv z!!cAqGH+*6HM2Vv?HEd6l1Y`tg`+|p&Va9?7FmXbW8RMMm7RCtdc!NASylbhqOD>a z#WHex0HNXx`+($G?{iE`H;x>xu4K3{$_nmq|)Ws^>{s6W8$LXPAm?5 zu`_8VD3hrMb-7(;<>iecl#)ylvc^Q?kY!6>EK3zTS4qo?7)WD8)Eg5>5j+cZn#FE5 zXU}~GU>vDfv^_OsE5Odb3!~P+VIH>gHu7a>ad`~XfmviR zuckpuA9I>T+R%)d3LZiB@VL59n@~#z^I9ZwRlvlZe-3T=Nro~eN1MmlK?QKQ`u}M2 zd0}ye7qshNCjy4_ap*j+$NMkZ)C-U*f`nqUI6+g~#X&a^`u;a>l4aF1E~l6Z{z!OGyUnq1aB7^v zLo@{|XgF<1+(zGPcMO#wB;XXtafN6oKm0M|+L`hNyYYM8%@_Q5D=SmXXIYJInnr5S zEyN;X{3(XVkKb%?boA-&mO2t=S}?s}k5>7U0d_nTk55S-reYTMSVQsCh@hjKN#zmO zs@!eHxt?5n7fzZZq#H2i93$>@0Qdv!bnCX+6U=rIx?SSM)B)f`hcU6RR(0CP`~clc zd;Y1XrzS(Y1pwziUi@C>T1h*iNWCvd4t?+M@b|ITWWcmjB#MoSdaE2b;-^{nNhgg? znKrs5wFVhykX#~hpgo#G>@Bk6z0u)Jbphwa3)t>*udB6b4Kcz1KoD!@5`t#1Y4!Pi z3*~90IorWdTW}%q2n|?uU$~10PQC=5)gL-5rE{MdD{rDj|1TudxY$Y3YEw zfm5uuBtxfRnh@WknCD#;p|QcJAX#sv@B;_rRjU>df@=N>iTU~6Sx$rQ$L{MqI@Y7` zlH80coGRjO_PMj!1-V;6TS?t?YKu0j%^2fTRo#=_Mth1^jVphl=O<& zI{d96nTjM7A=^VTkIgVC0hy+L{gxklrqpTzE%&rova0fjmX=b$Nm3j-&?tNriB1KB zX5t5;nE;|}4(0@TX?lK`?;A!3$ozvagFVEo@#c$|qitqB&eyL_%CZsx)#*^+lctp^ zBbSy{>4H~maa0p15hKyh2bHA;k^Xq%knza~l%Nl!;;MI{fy#5tq<8()6vEKvmDTLV zWj{0PP(KUnzlXr6XZ}ZBpG6mb+|$UzGPSC=_`D(#@gnM>B3xU4^j57U_~wK3JpLWu4j(s}GuF^z+#m5xj5%1-WVp6DZR3n2J$rYSgqlMC#;z(?RL2 zNUDVe{QL71B>EaNw3;9-twI_3j2zsGDAOcqa!{(kAGm8=r5h8=rD|4*s?O34i&I7J zY9-LlEPW8ACa%yOd2f!Urlz+eSYQ2b+xe@5FLXPI%A<+KICl&F?&%_Wj2?uj$znCWio8Hj)WhYjkk`m0e4T+wW(1Gi%YSN{t{!Cal8b zafKqU!Af7|!g;US_wzjVB8BgNxw}5^hSznTRnilq3WE3w@c3WR%>H1hs&1S(IiapQ zXLc(RNny66H7DjnPYZK6A7+Iu_G`O6G%-Rqd1^DCwa)T+oa32+i&E4Z7#kSKkw&mu zZdkEv+x%zu2ae2lQ$K{ndz4|Kz;`>GtSIoZsH(^f-1li)e?va=1h&mSUyhrWkseNg zdidRV8NUDftt62v#+xTlSO6U!A#2!lTmLv@^e2%I8=1E$Rc6B0Ha7{;&3-W!7_~3y zFy;|4(MbYY7PbS&TKsibU*oj$U#2WD^<0!SENYglV=*0Cm|ASjwDhJO;`nJdOF=1j z*~fK3^CVOn4n!=yl(?xG`Bb$$mrm5<4oE~B%X(Y;=EyQ#V$Kf9w-8DKnPW|Lchr-u zL~tgJJ*T&65f0hGMtGZ4znzIeV(#ai zU~D<|^210nRBx-tCCxD+fR@rS(#W>}$P8g)CV|(vcp$3HGdQ;)8&rQ-C%6VN6~Kru zrZFAZ_&_G9P=hgyO~lsKowg2H{Q=1`wBOAb5?OcqeE)b_M`lGHqol)W6!`pSDuENo zqLgMmOcOqf?j182B)krWl8*6tFZh{kPseeus>nYQg=@R+yV>$b&ujJ9v4NvN*rC%H zq2zSaH|S)(~U(d zdzp>W*)a8aJr@Kt&_8IF9(pyfilxeHolLVDWu)8=Onz6GDtMoerbv^M!Q*LX#-Z6D z?3ZM-doQ=wwCQ{7hT?Dq$Y=Dyd@|I-?xHkCLOLjVKtZ#^=ldO54Fn@41Z zyf|I9{E53Y>j2Y|m&A7(UbCso%^ll%YHjOCC-zv#-fsRa0`JgqsG()x(g#oxgLd7k zinB)G6pM>Qm#D=VaxtH_CSQLrW3$v4j{J&^ma~4nbntIFukEUb;&0ssGA`_NlA1l& z!S{w!E7-UoqJdDG1PTw&x!FGpFqO@T+y^I+CSmc4SZ86%kjAxn9*X&+eD$?|Vsh5& zh&ao5xxO3U43?2L+Y~k>RI0Jn9L6lUHW7@%4A%J;>Pl`MfyW4{((ZK^WQ6Y&K(S|D zRt<+E3zDj0<*F(YC_(*4inN&)mHZk?O+`BuN?ND9=NFva3Aj1bck-eMmm|yhWm=`t z9MbTLQKR`1X@j@5KieNy?)Q>~LpR^$9R(SC9_BMbK`l^HixW0qWGYSk)EI&Qn;s}?61ra@&bCOH zRzSG!4iyjxaDXy(RS4=oj}mYM}J-t zU1&x)TBYSga)o4^nY0iuA7VGd7QXv>?2D6)jnDOrIyvlUCfglA63&!&AIE!TiaH}e zndbToX7XFE1rHhMQrw_l{VfRJHj3sy>%BEpt_M{x-(T^=j~fI>Lnm4Wn|kdz?BJMv zoi4n&bo_8w%3RvAtF1A4i1D@rPf2+x5<6sWNbRNGEX+Y5jlUUA2leO|59*VI8UVk# zDGq-`4*j7LE;~zk@fVr5cz{(#BfZEheOm*4l@}FLClj5SFNZp#H6($uZ8Z7!RnMy1 za1_o>0AO)RQ|H5k=p$nx2GpIsjSU=8E5_w&qvIdfQUNkNp$C+Ae9WT9pAn*5+{*fI zSqpLxhsZVUaR2Q~HHP>Szl(QcS;!}68bVn@kUSgHAI1=(k@Qf6sW!F{>o*ERM;OP} zYeMJ?R|_Mcxu=DD9P@5R3J?Lb79S6B)?QB){j(IDHEc?bKp+03zw)pX)d8B8#*HL`LI@tQL8Y%V zEKWfhgRS5S#Tdjjr0^QZ!rU#Rl_pdNe^c6nbT?RpaHuO<;KJOe*}ax#%T}}~eDKl^ z!@!zSDkW=Bcawr=sQ=!RP-AJHBlpcpsaHt7SRCF1s6kVNvXSO$F7XvkNolnRkqpG* z5^aXz*#nT+o}mcD;BwdBq7B~ngo33uol{9x1>aU@v&?rneM!AIBo#8GrZ5aJ8C`SS zfVdo(9Qs$&v!B`Hj;5?W1#F0{iS`IVXyqL&@KS6HW(XD0`=Vh? z97}IFfLSlMsMuxp{zR!*u+C8KRGKPROtfCX#H3}kt;en~6f29#NTjnvgD|B$N2_CE zJShRA6k#1gD#*d1JRTvQ1T_Ff|CXMGpH+EyFspcw|L*S}&y%S+bjw3;Or_H;mM98> zDy7s=%_KYO0i1*_a};W%B~DOT+G-u=HieKNe)xq~wGpueks*3rlUR0vyTtgN?^8tZ zU$hSPVEc}lW@$KY#47p1qmcFU-~3UBhSmoLbDkaWlOpB#$k_B$r^}%=4iTXX~G_;&D3x3yqv+E2w z6&=>&lBT>sTDCmoz4pSH9K-GH@wCPWq7d1$;f)J z*lOw4ZeiH{YkFRWW1Gwrs@Pjs``R@;9e+>Nf zba)u~fgMnc;D6i;Al^GU&ND_URB0;@4sy^T@4bnl=RUaMlwslFWlQH{gx>T=3oCCb zS^{F)&eQCf^U0t^E{l6;rX+Lj-TMS|HSK6LCb7TMNxMwx7AXL|K85@c%3TEd(S~v(P6J^$bk+5Pz;p~St^^vCXC(0Knny5oY+IHzfP1Hs&rHNeU^gf4lHzIG-#>=! zDZc=D!iO9IaP0lO3N*CG4KB*{2*~8OR?Tp0-ES(%IW`@@Sw>G_X|xy(ifg37#LK4Q ze*=ujxJCZ@G>4?+F4sLwBP%agu0Uf{Z&FI6UeL2=gpLZO>8cW&6Upp7sdQ*+X8J5W zY);Z^YZd95XO)ISXN*id~6SxUPu9p;-5W8>N zCstXO+tc3P#qAmx{aOJ&1TnZ)1bm>lRjDP`-c<8M(rkwE~`@=ZSzuY7- zTc$eQiIgW3_48heJ95SDtgbE?U7_Qyz!1SZja^}zHsGijUMjU_xI@lq1=Aw~WCB!6 zzd5f;UZ>D5JX-HE*0O0MwrPnL(cgcV3p|)nBaF|VC;Q1=0F&}i=lbb z1JYeA69aWN!o{z9#A6uJL$zpac)!`;DmWg?7~v5P;Hs zdB;UjH|Gn_7XMg`f!&FR`8)J~+V}s)Zr9bHzSnQTf|950JJ{9%2}DN8mMjaKM%FmR z1q!^EY+H%({Ly$0-A?b%x7Xg=Zzmo`l!yGj2qH&t41Mo+E~!jQQvPZg{_CEyyvT&# z>Lwqfsw2@U4JY<|c;9lGh`ir#`&MCw!%TyPF|$c%@O`7dw_>ZNGC7@Es&vXl#wbe~ zpff^TDZ#}#Ve_K&3LzQYcnQ$otcD@jiVAGSIBq?Za*8N<|1S9_h|gn!pH2vapYWqd zi~0esE-Ri%T|p8JIMqL?tr!Cry$8L-Y$FRSR1!JC02O6{o+3=2-Oy$IwfFYCo`>LM|cYNqCriJ$y) zc-#o0Mx(dFJ@+Fh(~1X4QZ?s^Z;plJ)5XKb=O3V!OI?o%eZ)J{ZJU_)P=a?T()j_J zb*A7(0CeX;cn%AGcq=d?()tTl9wg=&R37$NaF_utz^d5;#Q|J!Ita=@fPxegX2cz^ z>y#Sv$>{$>)AaXmb#2?n-$NDN2DihVlFfknLzK(%j|c_tMC_Cs@0Ct0Ss9E&N#!D` zuM{0gXW#>JL}0BKU?KmP8OCicW}d)%SEwsN#~%9C zhtM-WM-NH~{o+SIbdVJnO}e`AbNMk$CEX}J#p|BrQ0ZvjPl9pYQYKgr)z_f?Zgh5B z<1x%=Y&Z^J7VAT)ki+b_Kt_bqZX`gR8Jbx z0uXMdgORu}&bKmBN7k76jDdFp2z=DThIx;7t$K((1036iaf&nE`ovBTA$X{Qdw5Hl z=CT;-aO6p?be9z@t+kpxEIA>w>ule*v)-3shP2%<%_v-AdY)*(mZ3h$gC%~`Djn`* z&TAw+r*ZOh`Zl0sgi!!Vu0U(j>-G!u9ST&kPt0qITGh5F)ILd6G7??C4lDg}2&IZL z$KB{_|63}@5H<3zOMBmYTpaQNV;j-X){na%y;+RDAZE(v&tjtX@K(*fO40Mac<<_! ze1yg2kDVHOT}UbVUi48bh#!%kfoRMO?N~Q`bJwjskG4!u9+4i(?z00T@P0s627tE1 zV5)8ZZgPz;S0K{LS(EvvK^X`wivTd#{CgAsuU4u6=vp9r2deTW8A}ZnE0G`O2xqy_ALo5@!?UGEp3B~ah4rCt2dsjwq4h_E`EZ#vOa&r&1xKkA2*s|}#&gz4d zEo)|(AE#jg16Bhv?FL8&56mQJxK2R#o%srL6@Ru9Hu8k!lt3ivFObd-hR4U^KX{4M zlZXWq7Nxq7igabE$@VFh<1fV`>0p-4x~_)s(dKVyfqOeX&s&^?g|4oTtCpqN5I?2g z?*%@9#h9z9NKT8nV%^8%TMOsIx%=Y@a$^K|#BsSiRWDCwUMh%nRjUM%{P&h4O{I1g zv!RV9=*1Tps)R+!@d_iGXTf+}!3r9UYS@VDs?dt7<@ARdxXa+f;y8uWV<4FTkAVsJF6&W*aX|LV-1gt?=j!HTEjYemdltCR^#GVdXlE?J z0H{Na5Sv5i#2P(}dy|9U#u;AgJaA(wQfGWkdn6tt2BG9pRH%8K%_BAHNr9eoHG9wd z%$-NS%WSnYTnUuXUN62-#*D!!n-|$hTuXHxtroXIpguLL)fhr0EuG7Q+w9njd-)n}B<5%JXy^_MSD z&1ZaLpE@wPmcD5k(eXwgjuW3>k|OHPo?UrYKOpPi%y8yPzo9N>$!#ajve`ykdCeR@ zer;G{%}sx$ApVj}@y;Rcqy_k$Fn{*}KS{DXZ*qOAu zz9_yd>rOrjahU`%bW9*IH6KguYJwR<$Q_n1*RbG$CtoWJE) zWH--Z%yx4fIM^a4Psb^9q75n@I96UY%fkVc>qM6>E-rBJi2v>+F}V_d5==c8;%f&+ z+9HgQhZ)hF=a|8Xx*?sJh>NO#Rg%s~B*^IyOW*eAm&eu{GzijCHzebhWK&QAa9ZRP8B)^F)k z;a^V*=3fH^Y$8abk5k*IOv^Mg{!x15#j9*bY6@((lP$BZkkoO+AW!S@KKCnY07}6q zap5SC)2NwLeP)I`E6Nbh|+ zuYBO*uC}uubQ)DICxZacW36d9I{0!(bQt=FkPQ<3K;hg}DcF-Qz-e0n%xH9CHGWWy zp;NMER~>azjIah+(>Fo;8f#UO(dgfSF8UN4DZ-EN&LkUrz~Z&AIg5l&<(ZJAbcxSo zTLo;iYIQD~K*}#}y*2)#Z|FXhh2F>j%dnBcnnb#p;-)XAO2XxN{|<@gKUwBGn^>Ly zjp7;Td1QYF#ir~;fZwZ&XAW!5e2^OrLBP|42F5my-1?^Df2;dM8w_lP-WK(5*muGY z+g$1rQGIt25C&h6N+heY_qZFUoL;*i_`HqtbMSqe;kf~L8C1kk?UGr*g_1m&Xk!=u ze5GcRjt{b-`2j}5DeRC@(&kxEAIvlDM6{Y8MgTKiR%HZtC>%b@=&!|NJQDpXF(tuN z5~aRG48PFgzF_7xL%FqJUs$S1nH&}R7EXwdEre_02b+8!f93GcQ0J`~!oYWcOzto& z9cqwa1m|Pl^J-Sjnd5tWW?W;D`GV^cp5w2iOeB>XRru>-j&3JPio&)S_d|0u&cER=h^i&BGOqc5Sr- zqbOif(pDZaxnX|SbY2JMPz;y~VSx~@`j;UGQ@NV@Q+pHXdp$O%7ENezK9I9J^_@~m zj2JI0@!1n)TB{2*%po|&$7L z(UEDHq`8kXO`CG2+QHa^a2|GdvB>QZHb>F8rA~iY!C}&bIl5hU$2S_i#&hUQ=*;I; zV}yfL5BZ#9Mn(kgMk>*O2F_b*-_8e^>ODO@Em1aa{478{kHA!-1U5!3GKGN)h1}mh zUYg|u;|cBr^#->Ol`mNH!lR{desk0N_qua6oLm%7NbGgFwI{?ZT7BBp3+DSFZCl!3eEadY0W3!|9xfsNp6 za?O+{q|L6}1OlyvG*yivRR8&kG})*@9#w;BR2rLXV~Hgoj8nS-=RV7M)^H7xX}wms zNe3H3O#LZu*hQlZgOEiXzh2jHS1<`RM4eYG1Sm_}h;@`$ndK1wp!xzx15C(NX=#=3 zfNgc9j4-WytFTq(YNhpa)oQJh!BccTvc)FE zEAr_hRSTRD_U@*wT3*YJR0bFx$U9V>jJtfa5X2J1+{`_z9?EwZFg&Wri{=cN*Uw>{ z1JeeJc@hj5h#DFb391?VqpjjviNywsaLWveqdTB%+UlP^MCqSC#Qm#9D3)6x(WHCM zJbpQd6(1kltoJ7vnm0A*NAjw4@-#XlTmgJVa4-t9@n7sq;|NiepG-wD&YE!4OC@s= z`;iSp!~xr}eiH+PUoWE!-g~gHnDnBaN%tj>WlTmdeZ9ck^6}v*yo6&5-}mpIVdB55 zV4sh*eo9wJ$7z?^9!L{gl&ZAb6Vklq8OxDVdy+RW6uzIMl}t$)ifc zW6^iS9Af&P6*ca|ctv({Ef&Li9?xdN#hMW9P@xT0(nmMP$E?RLIV$R^EyGd2cJ*rbk!86{2zGxDQI|A-+fDLYlmn7F0M#Pp`_kX< z|3n3G$Cfnz3)5x)G>kUE@L{^FuSL2vzmVwWjA=+Bet$uQOPC8%wLfK8E8PqCh#b(2 zPYB+2Oc8L*G7HO^TV_aK5kM%G2^tgBh6jWPg&Y0Q^EkTk`}%tvHc2XMNFOAxc^)KR zhC4Eq!3MiwmIz0*dUV=oYbG@ew931UUbR{?IQm2WHey0rKQ+mHq3~IVPvAEZt?4v-u1)V=zmg9J(rt_+VMP42KZeY!E6koFpk27ewN^|fcxuT3wrZ+wR!^h{a z3SAg@3#$H?V$VTvY1j7lB1Vv4?@G*l6NOJEK?pW*#FrW1wPnPOQ$Vx6;#Y=bj<3+@ zyl$}OqW)nq{@oT6JfotxCCkP2=a%>4^fbrD(D%F3!nhGkRXoun3rj8ohYj0S-8Kv2 zX5vHBiyS+pxAB64AKn!bF2KiAPieKl``F-8*8u{LaP?8+aCd}su<5EOZr5HY?%XXe z!Svi5E|pn)e;!onW%w8jx#&c^Z>gBNxMRre>6 z4K%+NxUZ2H@Wt)9T!;z2jxAo0)YJ0`wDELd{JY@(+f<@mPwUpJ=bU^Xngv-Bx|gRH zSA*-J@yH}^#?=F*Xdlwkid0(rzuHW0QC#A{V&+83MdPRAEl!CoC4;q6JM!cHl}d8_ z$tS_rG7u9@{wn6H#14(|1eZ20#!WQ&*6n>xqWtG5uChfb2 zRH|7ueZ99;i}Kn+Ul~BJ;bmFdet19=b_7_LCBc0yV-GLKu$rWcr{c@b&i>Qvh%l~g zQ$HL4Nl-RTo+=l!Fh*1XCrvk*++`*0d4btlMBy6{AI2#Km{zq2?Kng=Gl=9qB)`+A z*G|x-DWHF*Z&ku|I(OwVUK@CVh4kZ ztFv-gaeu}jQkjAcfOgMi`$;Zr`4dnCz6Z-r3!!%LZFPsUAsul8iq@t{aUv+R+0yo1 z(#Tm#*u2Fc)apD;E5jY=XvusD-|rr$C@3>8tMbI*Y2C1EoDKwsEsODNTEN3IV*VKY z;R&!bB3P<9Mq{4uM>-aTQ|l?JY##8IaY&ZSCy{R&{AGMoRX{sh&*t69u){M2H?2BlNM9pc5sJN? zk=~w*lM|NyKfhOxo`Y;}>)OI4$4$65XQRnLxy0@!I?V!jMr|zJqLLA@tI?m>C*z!e zRsqR$4GXfv;kZCsUf2&l7)?x8?zvP+j0QmYR6-=VN{^~N3YmqhqjQQP8#ekta%!_8 zB)E2@hxsAzc>+3lPyrqoIJ7CX{Yk?|@(2S|Xj3trGIfBn)JU~ z?S0IWs~ItQTdv>}DKAiH`(U31w2==1Sw5Hg8e_>av0Dky!oJ@b?Cfeztn0hC{U23l z_?|ak)RBIv5#KH@*J_WRaFlI^^qLRyF=+%GuV2*s6p1zXNU8NMZnZh<{em2fgW?Lm z3pi@0WBp1)u#Xp`&|p>DukPb91%3MiQHaRR%^iugTAf?T1a2^G6=w1d4EbBl^?M=< zN5eDjmvd(ZyX+RPGeb+y;^NEK6h&QiAo$h#^S>`4xn(H6GjuMm56D@)flf|%4z3qP zw3!EKV|zQ`j*gC|0nRU>N1)S%Cbs&sysuA}xnBRZJT;We?Ec~{8dmQ&oO^ioetq2B zP$~g`|1A&<2YDQ}zYG4$anUm@0042?ak*OW>~4`({0G2hFdbyhccJO+7WVh#L8dPr z?Q`}!UK~+oJQmKm08Vu5*x zV@A#*DLOMk@GD|Y@oq@p`G86FYxm!@Z|ZjTV-ljW7@28UH)}dFGPJ&W^rIPps7a`o z2FTi)RY<+B)Aq|Xx={^EY^OkQ0ETDHsx9$7fJq=1EIQm{n~3qN@#6fcZ4}7lczyO0 zK$A$G5bmB+bWbmAPEgEKmz3_f7gH|p{dFa{573k1QSP2f-tZ_%Rct|zk6=hvGqWi% z(VGnY-e4m|eXqf6V9^6mCMlw`b5@$LmbZWXK6cEdaV<=$B(1cRNI6J;khpO-g1HGI zi~7&&r85xOnRtS76TK=BI(?yiQ!DWP`8;)hk}wcN`y+eGp=I7`WhUF+dl2^x_J^vf zYFJ%CK>-LMq939?W5>(KYHQplOcDWy?dHQUgLnP!R+HJ#MFoKmJNN?UgHd-E&(qAQ zR)09hSbyb99*bpFe;Y|2cK**HO8$rh+Tsb6hB+$GAOOMOY97zcJbEYao6f!!bjdPY zA*~D{9_|nt#2eVyTxM9;szLOZrpV5l2>Rcgp0;Tg6{(9#fD8bl6=VgyLwM3Gzq?3W zU?GD~Z4@<9Z3VkVFhXH?0c8+{wm%SH27IP7*xH(zPy06AeQ#GN{`PP>UVodmbN$42?}0k^+KG;`GxI0iBJez)BdG%y^TB_`Wn!H2AZyNk75KuYaSw{79$vn$E>wtST}UzP zQF*ZBeE*vtR-@OAqs){?cJ%7Q@h$P}dqKBA_<>GLY;3&)-(p$nBSi0W_s!m~{Rrax zi|~Ey8qLPk?ESMP{H(ogNm8P(eyzT6WTmHPeODHyBC&52bK-IBNt!qcHg64+^nWKO zmeJ7v^|JOCg_)@#&U3w^Ordf3XwKcSfszrp|ot>H(<)0u^&Aez38dPCP}?4q%P z3918^ibv!hb8|>J5XSz+4ZF>?K6Kh&9}1xQgDlM{!^XnGRN&Nk;ZlL(pjXZgQXmgg z8is6bzYvI9LjL0D0N2@TFYy79ohX9Xm6RrVmhXnQXXrw8>z_jIcB(v2gXbjYxr>qk z1c_8*f;aD*X0Qz|g5&_8857v{K)Qi}ps5$2eI^pjFBt&r5TJ2hX5C91Ti8I;7};2E zD}SqOpDIy3zWb$kj_!1|UH#3pX7j-*t4XaLeu0Lq*?YAjPsf3t1eSSwjhuzFs-S=d zBdH;F+-UOGm@TnBE^+o}hO((j0|kK$Rb?|nl&G)4JTsb6=&j>^Nok3@^&tK^Wk_52 zRGs^C=gyX=_Q&UQ@5u*#oQ~cyz?#91gZWSUOKb0kZ7v)-I{I(ot{~uv4g_akbN{9v z9y?~|S@!N+go(5I+nLu4*FL_RN-H}C(dSg;pL6;;7neS4vL6aXH$JpsNJ!^0!QO;X zH-1tXoCI3R3zEWf0p18-wM(ZMrK-;ZsEpVy{@y6E_1F4$g&GLE0mrapPR`@if&(xI+%gPSzah;cr;~9WwCDxmcJ`d zSB-Ia5FMb9k6#XAMMBBdP@QWtoEZ%P@aeg?aAATx{#$Gg;A?s$Z@;3GnN7ZFa!pL& z!2D$MUuMT2e}%X!X7Iv(=-e+#k{3q7_;FYaS|q&~#w+-US*UE5sVEA1gwgKlTqN`} zNhRE1T#*-B{Ms=&tN74zJDJAioMz1vkEU4> z?=3;wd-?i!p*4|InKvi;swQWNv%V@?k^w_x&*I!1yZRqvw@zi1<$IH|Bu5#3G&uXZbc#2kp6kMu z--YOso1{ZR`HhTv!H1fgiUE2nPxw5H_OJQ!u22au9jcV&ONZ)N?!UlefP@YDTESUINt&1 zN$0r6PB?qpR38(eti2C&ULJn+M%C6A^ z$STK@S;SU16kXxojgXFQa%qE-DT`98#|lu*XTk;*ubdEQzg)C{VgKMt_+}lQF}s zhQ}!lnYD+Ls&PY&>%cY)Vx=%MGs`|-PwIa*tTzv2t?0VpcT1Iwg@QXEC<)LA){G;S zFE>n-SErdfS)BtVjT)2*BrMEBd811YE;ZIj02BIXRfi)c5-LE6lsJu8P}F9NEGgRf zJG^Z|1Z@{d8XIy1$+0Z2>Si9w2!oMy`60TC+Hzj1EE`Qflr=r_AsXrE24gYN+oPI$ z$Y#Dg(FvfUgn>MhSAb|~X$nWW%sDnjv89Yjk4ij0k&ZY)sYp`cE*V}5d|-iDc^Hi& z3~^LW!W;X=L0l|P zeZcw+`jn>bSm;JQ#O7cFiHRf-;AO?Z>BAjrz1d=}Sg}qHLpo^N7ZcX&97RY8ngZ<^ zqYp@;pSj7>u25OXX#ImGOz6}duz5{5*tmw@D#Hj}(?(A8w_z}Sd04|j!?sL%Rbp5a z$0N~B2|$ay&!PK?=s5=#d!EAH%jK>GHWG z*eFQK1WVB@qsrAL8t~bfQ4d0qb?%A;P%PFz=YAE&*%_Kl($M@`09-;Age!Wnsp-m? z35SqRr=M5akOut^0XRr2s6S1Z%hKVz0ZZ3V?JayaK5TJ`%Uk zz|^KfL_9)L zc)~Bd0}!xQP6@_%@bEN>`V~{njx3f4cb5AZ+^>7JT2Hmq<@5p*S95Q{G{kM;uJk~R zAH-(tFp}P&BU2E8fysHZVZQ+M+-)Qp`*E7hl0R1s5lgi(SkYmIq(*?|EYldF=8Iq; z5Gd2vq>6mdE$8ybcL}x>8D`%)g$6PW1d>b+Tav|p+o*=5J_DSPX}@yO7b|x85W+@C zI<5FIElSjSlGUj)3hRIqMy6<)2%45BP(AW@&Y3t>1SR_8co(^XqSbe6J!Q=?e+*_4 zQsl{UWqK=C;;14tJaqFKeN~2LwN&m_ro}*;+_Cj}W2G%+COKt&d)U;#$z6N*acvk_0ENNQjA;8F1}y>)K4WC?gw4DSbUp-lYb zTXxK+o9D*E{|myKGhvYW&mAkaTMfgp;c0e)aRK%HC%h+I)k~}EoL)gLik}|>pT>_B zYVPcdEQ|4(@D0#W(W(30k)ovgM1Ty2%$G54C4qNq;a&@0{-bYRPycCRz|t2QmwV_$*|UK(L5!qDF#RcHksLf?gzKf+W9Q zprD?8PdMm%Gt4Jg5USq>ldbjKdf!&O*s6VX^})!1_Eh6NjN`slY)_zs=P!A~rE`&#U(;Qdlr3D0s-!r*F-5cy=Uw-1z9K(8~h z7spU4`k3p+6OJEs3~&wj#DPc6E_g@US`2z>w5SpktYWxnt-23L`?QsiV zv^ckJw!RRNke@6ky7&}BF!b;2(9p|~pI$0@9Z;boEeNC;)n(l7?D*bqhas)E)Dvl} zcbh*K%lul6C8<3Dz7_aL#m+*Em8O?Zr}K_bGd__Q@DQ22Mn}xd%=mrYnoL+FsjxV3 z$?%N2$Pop^OUI8BN-;o!I$LbEogUAB`?{X_6Q~4XO<_l9qEL~Yru+o|u}nPm$3WVU ziow-j?QQmjO4Jgn+YW}hzRm%nsTCe^(x8#o=F!~T+>)gZ*l~wPp%eWm$(xf9vlj=Y#)7xUAf0D`}6egTuXKX2yzv_z_gBkd?qJGz zx}=Kt5I=#T!#;S^HfUL9VY=)WY|Oio1UM)fZery9w#iP96rbxu+`WO!?9{3fGSFx2 z2J~HSwctmGe6BMGXFB`-lZJjb1PaD!Mt~IP*jWbA?H1c_a&x=$9xf)yko(U!I}Et+ zPY>{S8ofXYvC@SICPA`sr8(P5mxA9$R7qr4YJWuE24n&92bMlRfd8*V7k(m=U<(Ko9#hm~-z~`~!I!i< zb6t9nzzYQwjJ>t$``xn}Om}jnN9B0;~JmWNJhymbf7bRaGlq)osn1z z-V|-S-xt5aDmM;Uqw`4(l4y0P^R^UG0wmZ&>qFY-b&~;*eC~F=S+4?lR#!k6ZK1fG z$Kq&bV`IMvR{2x2ZER$0kyL1CHTCp*z1ccJ76Ow&uLu#BB(J}7q8P}nv1mjx1% zWgRQjN@4L#;dv^B6bzWWr6e=Pc346}0#GeiWIh8m$YHuykPZ7sHQB7mWOTFU6+~B2zW! z8*l0DXqZ+<5Q+Yz#Suqr@EOnXAp_i=D>CUQ9H!WPhMs)F5RKcvM*y7vX~M z`Bn2$i=s*_Pm2xLwfS*O94P7uzlW~d$MZdeW;>TLmmL`^MP)U`t{v(O;i!)v9{O@S zm^eQXWbwGkdxA^tjSH&Z==_#q{Vm>1{d?@9Vv(s?bRs_PE2s8vX+Dl&LY{$ZC(BijN>K z4xa~t_RmoU12mv`E?60(nY94C*Og3#ft_cKBjooJbk|>2gDR*kK-2vkShDJ5o^fV# zi)rc?vu&;X5qs`O|Ia3l`>~5;)?(e0+T*t06he93UGlah>P@F?Sd7BDrisDoqZaI! zN5YendX)2*vG%>)-X7$YbwIRwX1n(cSM};;0TZuwWox-(aoo zJ^Ot{|7-A1|!SWnBx)a=1Qq(UQn?#MllT@HcI!0j=r_L=eZ?O$euF>zclj3*( z;}2~qs(Ck&HEwYKpv)#55KW`Zv_NT{K1S*WFZna;p41PU%W)qpsNwB)3c-@|Sjl>E|?@&vOocqQXogDN86zi!Y z)$)5i!Kph0VL3mE8W!k_dSf7}-POad2BvFaIsAPRn4O+}R8Wxj`gmhwKz5%Nx*?G{ zzH`u#YHu-pPApTVT$izlnrL;MPghD>7jh|qq@k-8tWY)3JQXHI+>ku3Cras3qZihW zE;qb}P%Q5#TQ*eqZ4@+t5>6&Nz6{^7jsm~_L2wsO3@3+ry6IeVZKyH~XfF!9)!4^x zSBj2BL3Mqu`t=&T$!!9=_EqopRHt;Mfp?<~q6ISqlW5vtDAkZ2eML;${{${6nMu?l zE{R4&bN2>9LnaD|jt-8l5FQyC2!cZ<8WeoTO@Kn^lk%+c>Gr9xoLz56ua@&Z8+{Tj zFk8>Z!O#Cmwfp(;m-lHlTVSQ8=qpFSZ~qWFSLf}AAh4{d_OGm&b}=2t?QgXWXQd)d z9GacIGl99rk{h!oN2vybh0gY(ZuU)@wWg;7h(4`C^cx?DxKW)66?p9*5r8ONg$SB% zv;`kt9^xWb)|}@+n%->JFo*kH#}~0{fPT!G3YCWL$Tt2U&{kPV+k@8&R|R9STNaq6 zkv=cLlNt|?3ir3p&M_X&yqH@G@t&ypa4YB@_CA`FF0gIHvDP6TLGt1Kx$P&^=Xm4`C@!;_HU^!FpGOIQf$DN27X66|g>b0@IaTASVj+0Zv<*9GQ z<94AM39q9WE{=>Z)hx8pyY3^AJy~^dkuq)=-yG)DN4KC#-$k%|)itI-n@w*MrOZGh z5h!(1>MKWEnKpr>vZbf8?*(MExuQ5zF=I~e^OVc9GTMRon0&CC&jzyg*a4JcjoaRE z<_KRrwt)bJF!nK{`RQpn-^3}k`A*RXwT93jjP=b73wHubL2ATRWo4Xmb=Qu-+(;$p`$5_*)^E@-jy`-A zoWAv^%N6q@a`BCgjrGDn+PY0gdr@ICFNF4^K3pnU|Ku=lA~ZQEbdW4O>3|uWKw(YW zN1)<^JxL*lS7^Eqt>l1pUW2Rw#wCYltbIb^(`alS{Js_~oi~Y7cGLMMSaYzD!c=m7 zW21<7Hnh+O3qj+%#VT=d^W)xV9HF1t zQ8njDrt|s5((%2t(EJjTj~DprW2!7#Ib+4#!Yw*snmMuW>9_Z}TK1Z{uKGbyL76WW z{^EBIMCfQ?=9Qkl4}c)DqG#NbkQCCD95J`@vT{yJPm+9N!I|64xdXs%fjN>H} zfx`7;5AEC;S$7Dp+v)Y&`B$2N0hT6)g&bB^Mhk8A>jl&@#zMkPO+WR7rMP5m~ z#Q!WqG^4Q$ySr9hqNJHX2aM*_QWfS$Y;*;55g$@vaq_&r;0BfY(bEGbnVAV3JK%K4o$5-5DC*kWyEO<1)6 z1~)b~(%130KMY&&2;q4agUg|Vi${M>;-u-Uywv3S-7L;-@H7eCly52sI0w^sk4d`o zGF4JKvqMU?OhC;9V|Rqf*u%zS?rmGVxwhGxm1Kg29m72{Gt>F=z4;t^*H%kMC{wJz zmka#+?YVdM5|`W%OY%~F`f6?Qs|~#2-{;aYB?&}qNWorFuw+k;j&HMzV1*4X zVxOv^M}SF}k}uY@!gacaxdv(}@$JfhsuDmlq0m3j<^5(9I9z7+!#_6)lfkN+s;WRX zgV^2dIm52{Me3f2Lg#^A3;q|01;gYL;&FWU&$QjQY%a%>8GIb5wcoNC6{nM`X%hs} zTR>P;CvByAeywh&_zs4>Dh}mGkz;-p2%+IuVzBVQoaXx+aB`tEA3rRuV{%EAcBjBUqrA^a3;GXN>MOzRf1^Yx1D>y~*9;n;bqqO>{e zHqn`cjd89#yaN ziIDcIn@#OZ#r3Ga40FsOr{Cpi*mtsc9q;azO`i^3Y?6T!JC3g2GRXc)|4uRr)UW)>8^yW>F4yWcr#u8|7FHCs z+zrYToBp{vs%U*iP{#W+!M``~jy?IUCWcVh1t9oK`xfTf%YVApi1l7iC+m1S2{#@hc8Fw(@SAsQhZLk*YQzLZHEB7qfs zfO&jU$U;lPYAOV#bXP}<^#n7+i)eM#|HT@F0`ww3uU*FRLiKt z*Tkhbr&i~+w^Po|`{OM|sPtMl`4!}^$V{3v;>S8jQL+WBe8$yG$_lQ2n?wJ}CSDFt zv|O{IdH!kfM8-I;kk9TA*-6NO5%FKrKR*y=ElHgG@ty*~d;jEcQD+SEcYy?^Dam7y=%?aqHZZEEPKUbsU}jf#d20{`(bna4+pj zR7sq`0mamHqp6Yv)zrnV7e#2A#CO091=NxZ81(+TUcoW5$eEfsuK-2JqyIO;>}bXpDC{Fo#F%^^XaP;?ocPB5;C^w)o5rXk;l6$*Dq|F5 z(=>2zt^XGX(wGQ&A?N;NT7n)B{Ek5l#(>fDv~vSQIf){{!5;Qyuqn2{JPDg0?VEKt zl;jVO#tdG{j=A~EUD@0Gio}Vw!MQm4Mr$0zw1(8Mr(4p+^~WR^F%M#;h7UL~M-w2O zFNfJsP{88b6aZZ*WH6f2Pj3jB&A4`9ty_ji+xSno zf%M3C#%NZ6Qzt|(__%J}4pZuj{h+9zR%Vk_9$YKXX>TnDnG`G051i4~#~hjQBMdLO zmmK?PITw;4t_@P)g}A1FW^8N>#!y^*BnG1Q@0Imas(1 z$-@EO6wer+q*ht2%?6U{VLr}?sL|xGwV-f6k}}wHnWJ~jh|{JxkF1VEKlo`)zS$zI zq*J`;xl!%>qCB6RX^DAWb^fd%VF*x*{g(5%;^LFfGu$&;tu(CLAu>hGE;1w`5+js<0)kAV1eH$ zk%~8)4V3z1xrb#Ai3|26Ug}~YK~<|Iz|S?WSi(O&nzh3^sh5fcSfllk=5_Xmuq;S!TZhmMCTkURPzr)YW zJds&dj*AIwCN&7BB_#9`c+bP?w;8=MFQeT$BRo<2AD9hq*fmk4{`n1X;C?+!LMAl| zKo!g}5COjvw(z=*^HQ{V{njezP70+T7|!g6f?XqjNutmWST*W4I+B7@1=C7%bA{Gs z#7;A#_#_dulQSwhvte4q#_Tc3ZEov2JK60QU&p~7fvGetW}E|<`PCbObl;nU{`Zf$(WLVB1tTL``<$k`V^xcCpSX9c~5!hhrl5(=`ecH%xQ z1i4-gCOfbQm{HK>;IB+Z*+>Pr8Om`1DFb zWMauRrTmZO4NOVChKO!tEKRXgfgph34usu`-x93Ws7r{L+n!Zg&2K<_k1bi9XAw;2 zSC|m`HX}N-5WxDlE&=|VZlZJte+?I=+(?`Jn#=OT@PJ-6aWj+ShMpO{@VPcGier*T zy^j~Ze(JG4%*W~)@Z*zC>rL|sMDBb(wK~^`TrS{2_Hp(pYkdD(sEO!ea&n0XO>pE-EBYJN)dZN>spGlGZIs%wUkR z|G|o5JPkt0{^byW+Sk1zYoN&{CAq|iDi|zGdISjY`@GlBCRuULxyMrlkP)EeS8Efu za(pGBbQY$fzb8r|FVlf_0uc-e3I{k>x6Y$H+f4GAf^K5E48b#*^~x6mQS_HzVuBAm zp=}!J5pXb#YHW(AtLiS2D^G$*haH}eGzfZ8)layVIsm`!%ZABRfF{a02EehHFoBUL zOb9K<$ruJS{OXbg;{Nos;~>_$ZZ-ye@6eOXR~&&4g{No6BzbhWn{ zLQgc=)Jr;&h~QpcZy4_(aoq~_vOW;Dft9z5staXVmXv}(kE3crTaDr5;ofR1HZ`CV zk;O3xe=2t$%Fz{o`DA5{B(iA|gP~RM=8?-y%-vWj_a}g)3S#A(+a<5iN}c}xGM#pP z!H^kluwt!pwUVdi7>pMtkb9+^=x3HkyEwC6-s8i!nhFc_ee!B=&l~_jeV#CQR$}+T=2CS~vVQqbo zrfU}TwEt!ByO2E}_uk81uLSxox0{4GI*r7>%@)_ijPm3?B|iL_-IZrwlytsS6BvqY zZ~58(FrbtJ$4drnqOfHL2i#}5gc>~lG*Un$BXK6X9XHU`y;aRhN=ix_jE#-$i~}{g z%EwOnh`Bn@yB}x@5fQv~_%95&peF19IawZg9^uGPw;L_!WO@BN4{Uhfd5tP_B(dkE zJne+&;rCF8H>b$i^gN6;J$FF~U!MJvS9pdgx$ARz)+gEBQ<$J3l|1Tnh@2BfoRTDi z97#g!EB}tb;~Ig!rGuv2loiGbj4~498&s;-MBnZ#yFiaV zbz|k79zZAcY>5bU1Bbeu$K~x(`eTGAUJ~U%Pgx+e{6DJRfjiHr z-P&$!+ji2}w%NwEZQHgQ+jbhOF&d|_8nn5SG{$>n@8{X$`+h(>MsuZWook+R9y4$9 zzjMykdr%U}rqeG6l4bP6EVRX}zIRH$>>(A}O+7fB+U*RedAllYtN+Ho>MOQ8+dlvQjUTY7{w zUtd%25tHK&jRL2)U=+WrFHqYz`S;}8&S!48;%cn;8mfi__Ov!wTB&2cFkZWDAseI> zhLHRoP8@Ziv`@sEkl+pjU@(YrkbavZZ0!Ks1JsLYwV~0Au~#F*gQ%S?wTW16In4nv zSZmihnDm>BDs_(=Qi}u*+}1V+89rH-Qh-u>L&{;k1^-3XlW!55TX(Y7+Wri)Jwf7t z`9(JNM-h?1)-5$v_|Ak=*3?GXr5()3C}xI3uyy@iq^6q2hz>3Dte57H4`czC;O<8% z=A@P-abJ5GLN=q@^bb>e=pcZvpgtd&d5A;$$#F| z62CJLh=?2SyHCDfKVsnFX_HM)Omw0VO3itahKh^7#F$R0v5BlnBa6_)B@WV;J%0GD ztuXy;_6^udC`y>B%)WpcM8q(p(Gy~ADJw*WSiei4PBo(W1fR~nNzKNaWUs&Kg5uUc6B}b!{MnZWfE~6mY_f3P&@XgR zIk{E9}L3o9w>_FKG5c9;~@(aJ-+ zwvl(RnyD6ZF`|45)WxY<6c`n}$cmhhyu2(_T>2_8&UY|0xk!U>>@S)KO2^4I;6w2k-r!Zfs#w%ZwI3S zN!`@^UaLLZqinop;{c_jPSLv%hlQ)T67dM$e*QA5lFd%#OKw{3=iJp#(zcx=&|mPI zjUjCJgh0{Cw}nW}im;b+u_&GKmj-CL<_$h+X;U54NTgjzFBrdK>R?@zepih(HG@KQF_gg#G?Tn6=J(V`5+PKC~ z78`Sml%`iT&5$nwa_NGE(h*g`%(qsXD#C@s9;i;ffJIU_{H+7c=V6#cDYJ!@j*WZ8+G$IY(Vi7K;ykBRJz3j7jiN7u@RtcC1b^D1g%Q!>`@qC~h_)9Ux8_8hH z5wJfGRIlew1f1*c;8TlxM;N;W#UFv0f>I@TD@AxExp{VfY#G?!@D4y^U^rC3f%f9Y zsu;eRrLvkZsGWU7)G50Hk0|}7>6T7M!w|z#*xhEcms-@^%-Oh+{(WGEx}EO8fk+~}sl5BHLi`Sxe|^csiKYmVK;PadVO6FfJe+g{ke{?&WdX)g>c zh3Z$CT+2m!bF(xa6W@2jz?C0U|Mw*Z0-Sy5C8#laWC{xK0jn^p$mDIF&l@rBo__2R zQMLA#qmx(!<+5R+{rlu`Vx?^HgW(-%gbif&zh^MIbYNqAw69}|p8=Ov^|g~Qrr5NJ z7Obo_85F0Hs2XRZ4C2Om(t7j{Y*9Ds?aUV3W*GY<=nsb4sYb>X+|gbUS>Ne-prtCP z&e0!1hz?v3rYSMK_?e_pIC5ggDdKd%v+sr|2Wl`gIdo=lFx1#=y}67@fBX3J=ev-) z&$sICdNI)~I6KE&9go20T7@|I>vzJxFt)c|=de2h!g)6}nh348L6+53;!!2uO*my+ zpsiPE*K?OAb??+=nXn~D+*Z6KjQfO9m6bCZCu#E7-e*7|)^A;ER7PnH|9b&z<|4yC5V+;tyK(HZL9PMPxYO8E4e*Z2^UT6`O^J##iU4KU zES!ac0^$Za0)ILbV+oh}4OUcytbG%|r~(Mr!j9*_I5!eEq`MP>i&}2kQjOUzqN-%p zkhgI|@ZuQn4M}Di24sIj(D+0iJY&d0;y}}b^OZG?XdpHL9V zobD&ebSN%?cI-0%fAF)pyGpANfFi1G+q!?hjA(XC&3)O?-G0IL?j-vv+N!9OE+RF; zh-Vql8HkvEbR%On5v{}0jBn(kNz9?((Z2hF7+(-SAWH@`HaS_%IuB5X`3`p4EswI} zy)V{PAw&uUM-Ge&(ZZCS6Feu|4pr^p($U5X5Ff{Y%I`x26Ys2_Ebbo zX^)&C#=@Y?|Bquu^PbCgs}^=DcM2>k1q+Mt-bMr?jiAw%946KOIHRI*eEL_Ht7XT( z!;dH0!KlgaqRE5v@|)w)w;ES8V08W8OZG5D-s@kippyXD-w0NeX2{A-E;22hD8)JA z!z0$~P*_=vMs-=dh|~Hzm~cKV6Dl|f3^)BdY1ugvPZFXBX)--w3sJ$4E$oOd#WHc5LF9moJKmm6*rq%M!hm?nNe5KV?xj43LQefwh+Nbm zVJR7U$z@vA76?QmbyAz+VrH7&41$d$ABEV?smuni5@-P6cg6IZhkFOB&UUE`A8PskDpN@x%4ZyGGEw8%vrmo; z8S^L!V4|y?g&=kcB-W?}ws+T%2gzOl&9WQH_Ww?3)V0$Wpp$H$AS>e&%L@4FKU34S zPL(UI4NYre#2#`nMETFJKByE5DyS3Smv5kdmka8o>RY$xEBwTsgjh{*l|Rah$${62 z=8>UZz~hk>b(DaU`3$Q|M1vkB4qAA}YI59!pe=Cr%zZ!RY{))?3w)e1Ob~wa>J(Fq z(Y$s;!WQ$uHaGwK^CDRJq0zV*F-0wde2~eI*k7>62xke<1wykh#o!L{`DweQFd-0z z$HwX;7~1&;eIkPzJr|XV%I%WYzGB7Qk~~gGC;h}b`;#3OWrwG@pj+?q_K(wkS$E-l z3c^=VQ>P&%im85BcBA$#C7bG}uv#ytO)qG2%tVqBM+WWOX(}*1bj7$V4 zcsVfp{jWHwH`vkl$izroXZY&=64G!0eub2XSVHA=C2|B1NeX&^SsWH7Ul2&>?qj2U zB6|~!Ccr@rsK?5hMX#cWU!O2jLFlI7(R)F|3dXj6;bSa-yadRNnCa}~`+wH0)8kKC2Ih;=3gFZUByGCJ|@Zx~*7O=m*!9MIBPe=6Gmdxfdf zrixiabGMMflO73BdZd!x zc#B~W1Ts(ICNkj;FbuCL&N89iZJ8>kWr+DCMSNz>39BQa3Xhiho&!&kU_+XwiB|s^ zR~u5pJ9Ccw$L#(3huTpEb*<-RdF_Vs9^+2Y|Er7u?-*|o}!$IJbUda~d^ZLNR5@2{+7Qt{lTQvXvO zlj#}GPx9UMXcB8QvL_=9TTpatTyV0V#wyOwBO#u!l|H zB!B7l{G$KX6D0jLJ|U?s4i&UdoS>6Mmr~9R7Kwza&AL-`+Xti|YB6_tC>tN1oQoyD z=}46G#X;^v5TrpK@+O!{b+|QzZ_k!mSksmQ>Ots!oj^CWt*52`i88W<1Wb9Y7Q~M~ z_-@=3MGF&Ekz8v4BHjI7?9wc$ki7&xA`PzESNrzoWk`N$=-ORIz;8t-OHR_0p+S!| zDI=jLgFavJl8X$Up78i-9ro2F770zBRrMP))LDFwm494nEB`diF$Vbk2h;k0C^}w; z(FizKJF@Vvaq~Rtcp3Pk!1Gio?q}g;*ERiM1{cNvSq=5hrM?T@cPal~9n`)9LGm#pS449GN%Fm5rWY@1kW+ z%2XnRjEbv_HbPkb2Hi7EOrv77Z$IgLMJsMq8Xs9BwX7UMKW+s@T_D8~67W$#jnwu2VmY7|*cVWK51 z0l~kOA3saXY0TzAp2u;jDCA@=jQap7sDOmn(yDA1E+<_$nE=VS>>|3^>3OxAO8XtMhZHPR3Y$_YF=CK|ST zH7E*Az1pboubfuq_ihP_xLZZA7Q zL|PQW9A^mnNEwxb=mpxYfiSwDp)zMRnVJHKb^y&Ym!`|jFe;V(2kaeiu#9b&A{0n!3m=~50y!sK<14|vh(EZXJ* z+Ei9q+;0z-i#AWc1dr<`6$W5&o{Mxz@bC|-Pqh|0VP$!RI#)XuWOR*u70GASrfD%O zY?0{g9>)Kyu`=X>KENzTbd#Mio+8c{a$QZP5pipadofo}1{f)FTk1u3G94F{9ybnr zbK+-%QwDd<4*aG>-XmzVVLvNJR;!n5gl^X76wLoKM4SHEEg^ZuZ64hS4n~C%(M$f? z?zr=IZunlV7P)82eh&+GN!5%Pt)n$DZx>u#15a0E&Zg52WVHi8UvUZ#8(H}#uR_3U z%OuXlO~e^QQuic<#yAi|VTkPbY0LW~oWml5LS2AH>Q>8lnf{y`EXB;b48tFD( ztu4^{0nAS#?{U`Ja*|SMp;i6bY}(C}V6k{%+ZzC1vg9<>fVeyU1y{$fcw z-bWzMN!ut2WHX}ux_hUyllxDkA87{Z`D_*VySC$MwB)qa&c_mYa7|n1>=Kstsv-SV z8*E|+2?E~2HMlA}B6@6y8PKOZcjDx1IyGHD@Ij*C(FEe$k!pOEPwVo>4xT*(4W|33 znuL|gh#OB1ml$-9ZY<~3%z2EhK?ob(ltTMxiKlmpU?^$^EXig5w+Qd< z^kd{}m@sCqL4;%(EmochYq%F@rj=2UY2YMXh1+H#kL~HPSK|A|EAt~h0mFV^mL}BhS6rV_`DWnLbiU{ec z%HI}YW6&vRY-8rn*o8t?pyiWv=&E~i)cN;7Rs=9ZI;yZwK20lP&%eJb)2ViMt$^cP z+OU3qXUDH?6LSy6TLuvh=BrE=etwkgR9^SLPb3DJBJO@xQHf^W=fHy@Vf)kwhLh(? zI{b}Yp=kccqTLLxcvxgLBW`)9k+rEz{lnMD6@1Bdi3}CXE1G=|ESTaY@y;%0mr8+W zJwspWxj$i7zO!AzkFB;QaiHN1JR>6?u%Tb#PkhL%U$B6+lKf$2c!%A`i$kVS@-WE4?T&W<9H=Hz$Ao@ctg)BAPyQ<$&S>JPVCXp%(-+?#k)&)v0PFGz(z(R^L5lbk-X-ISE>SN>207 z3O(E1#ge0P!N78l7ou8Wf_DtlkX>po54_q@YKRoK(a+EZJ#d^sY-?!FjWgl+Y&4?(#jiy(ti;NuJlnFKXHK#Cg0o)@PMuYKD%3U($LCMc^y7B)Y98XF%C&LjHDt}U01G0lTns`z&aOisaP!bYZ${U%-c&^atvKu zseCxB5R&BWwK>E$zd33)9=_{U9ucIorUycEB0y-ai8Kfr(IAk)un(z2N^HOc{;7b# zp>xpiX^})w8Lh?)0o6KW=@N3$!>e<>Co0PqDyr%TgM57$(y<~F>hSyNMrV3clk_cT zgEvxyxN*gGktq22ilVHn1e*jtrrUwlG?1c}))&1hC($>Q6{2cJBJdE={IR045q(H~ zLJx}}F7Ox1%m33{#n`fg$4NBkZ)B5Lt9LFI)X4FSencmso`|W@mh~Fo(>SePMH>KB z0AmLwp`ACJqT(&Cw#M;e* zK%oSMXR!uEWuL`VKZ;?<;lBvaty2y2Uf!rBZKxu?57(vZL3cNt*Bh&S9xH8g0WJx8 z&GIDRQjr4eKRh5j>fYzXKK#a5AAaMtrwLhpG@5Pd1v-BvTm_7)5JlDUTU%~qa8Dbx@{r`UiGNqwF6Gal;-d!jueH3KoQ zaih3RP$;;W6W*c7i|LDl>b0w{`BuP1w{wCp=K|vrTIPzJ<hZuRsjeuO$$vyqsZ4P>?*GjzHKpmH7)Z82XIqP-%AR4>Gl4+MUiKNVHz) zYfkLHa$0mW-<*bO&GydE2Ov-+UP?ThDwAoVjcdF)IduN_Fo+3v2}(L3$&%TFdDDpA zIllLO{pquP@%G2FKbMn2PBLBotj%Msv1pHiD?6{_*I)}lriv8ojpHQRHeUNy)Vdk( z7}#GOhK;XVgL#GYr8qJaxN>{`T#kQE^kKhJJpep%@Zuu)51~2)= z*v_oV_2S~*sU!z_JhWh|SCf%`1d+V#!nDE==Q*oB+bf$=w<{l5eWv3t@vnApq1;!k zcsdw1KO&LzMo=SV-b7!pPVFdn#>%uVv6xW%`FsD5QcVIzu|l!RdVXsmT!e}jN(ifQ zKjJe)k6e^&ufr~ZNOpCpU#2rBDJaf*W|2SthA*Zci$1EC8%kG|N5uov>lMtT$bvRo8i(<_2-sk0<)6M(e|LP<0v-@>z{8#}6n zIn(9nZZpQ+t-OaIe9rooHPW;@M#Lqx4Ah<1`^hkG^~TiunvvY%2aKd85SVm)zgRP- zDEY!gDxJ>6M8Q-lDE*iv&kJ6gm#isMfvJ%}&t3fu4$V3$@pF3GVapmgMd^DN73eKi3T|xH8uuwgtaM}z6z_uROi~S zLbT&thi+?)r*A%IRT%OAzMi+BZpV@-_dWmmTMrJ{tN-Vezz!3EEeRC?XYLywgXXt; z9qAtIuA&Q=YNFP$cv(mcfQ%gJ6U75J_>?M{)J2p$(7l1-wA$6VAD!v2>(1E>xuUo0 zvisF*?=>k>05Wocd)w>aqpzF;0(R^h0|VTH zKanOmOWubtxX1q+P3nuZ6)vXGZV<#NPd5a?ypU{MBF%Quh505EzsvKQf7vTb=foxa zrERJ5_&Q%{o_X_~FWs)ybQS$gd1<>qL{k^#*7jl)mF|m^j=nfQM(#S)~8e*v(NR%^Fw1I4gLTp!2<{ib<&^x8HniR5(4*$ zNYiYZTmzMM!^hoqehM%Xq3&Xoc5_XGl+)_=~NJo9i9= zWXZ028@~=g!0Wl;ffwpzC2^SCQmspUeW5t(4&qT3k|g&7EuHdCw0LkR;l74kwxffo zJdLP`8)~)vOUzTS$pEaAeK2iek4x~lK3;(-rWEuO^EA;v$-5U8L;l&c6!a~id}KPy z`yXH$^6Wdrral39^~VnOuMfsk9d`9dbmLCqAso|lW$H~j-!l#*c{EcIV&0FPpb(Kc zdT>vpz{&R1EVy3!&KopIDYbt#iF^!VG4vm7rC=2y2xKr~sHQNPFp|`Ca5Aw%aI(Kq zkKZ%34+3wp_fjDpZ`fTpNkwR>D8|-kxfZ>}qx$ve!2Y_r>|z|mP$!Q_yxI2$Z<9@s zWi4>JNlduMD9gZ(C@VoU1$&tZ)bkxt2#Tme*GTxSz@$LOs1k;?)mu408a=W$xO~(l z{<-&8W@sZ!TI!~oDdA`5xUw+~%8a_*_F4i`A+_v_50*kV*(K3tJZyt23E(yyP+CXR z{tj6rn~sTbkn-@q=9lmfo@gQBgnmQqo#auGJ&3%6WGzM1#E!bNRXeY-R0C}$Eq8*AC)u&!PS zVdF5oj*4a}e@G})tXs@#LBhgoym`Y55l|`K%~uXuK3j!8uoX+2_hNGc3Ro+|9!diM zXFgXl?=p&|dDTp(;Knu*rNNGwPR_-RM3Wk9=4QD6DN2MPnj0)C0e5<8+_rQIf)Z|j z63s-g^eF(xaX=`qm(gP3V4Fu}@fIQPV(ga8zzAGL3HjL8;mLMIDx+J`>7B zLt*%}4B`zJ*n52K@>!P4X-86<@tO(2LXZoU5E=!gxMUhMq)n64o?7>;C86(15&k5$WA zcO)TYnzx6z{<*!Qhlm_C#+rWg`Xb}m!TYv^WM<(@TwdR~{@n@?W-MgjP0y!#8n66xYIT`*(&8uMggdg{zSfA&%7+jlQ z79)>Xde?=)PG$DgVuRYv865Wbag?&fNPOa*WPEa2@pZ-~5Y$x;5Dc!s<5=zuqnns> zN><%eMmI(lqZM?2=!Y;vb~w&EUqAg5&%$lb&-tZs!z{*hs!TKLJT~=LuxTgBw%5+r)8!3UE@5)wdFR4A6ta0HXhL}@ zL<^cbz>{W!$nSp@N)m=iI4SvW1S>Y}ulHYH7NL2^z+%38x6i-QiL88L+hhKb+1izRJ0G7#7 z3S~4}nIdOOXUXq%WatmVHu2+VXx?#(H|vlgs$WPc2kwapY!g z|DT&DELB6CC;SsTRSO@K^aM(vbg+}{*t&5vDSD|BnkX;bd`d8+)JdA8B($PEAnRqb zZiBQjapyWDp_itlrC|GwV2L(N>Q-+ePeaDI_do3V_$+l`WGfpNBDYI(Avuzef!Ilq z8zqTCPlj*q5}sFDe+FNyQ7Jc4#y5OFZw;}YE27D|n9VNgIeHBfaJ*BrAg9AuPfwF1 ziwiMsY7bRded!vKqS$nT{+A;E`4JeGV@ZG2i!vQF3EW;-3U7Wdk;&@vR+2W0VA5^h z^1GSA?R^b^@dM~}!)BNYz5M9%&vNms#QCKUZcz^LV5@5(r%p*AlP#)A!n4^#%E{gVXCciJ1+!9TU=8{qH zQ=qbqpRV$6gA%ZVRgfCHUj2>CYB4OWh?1H)9+K;E zaeG%tguTcl2V5601is95^@5XtE#-_S$tsv7y$f9}!4ALOQ#Yed+JmaRE&fQRp<6S4 zR+F?#Lq!`~=ldHUqam4YvCy!(RPc72|L60@vK)%<+AuDsXKkI%cboEqXbo~7p_ zL3XHOB8Xw4)UOJW@WtfcZ3v*%4?p)@Tmu?7?5khg!^AXekF33L&OlbpfF|Oqf$ycB zbx0efRIsZpd)ZL}jHCqP3FdFNIa^b4R~N+fPgr=+Udg5(RK4?M3yzOYDg6XI4kCE=v_Gr7c&ViH!R``&s!rv<;+S`bD{m>04T9udGH(|;8PC*aGE_7dCR zq@ZCc3xFhI%GMvx7m~O=_>kfgDMMwt)bzH{ugSmnS|E`zt88$FA^YDZ~nN856 zekKJlj356&@Y6+qkPdI?t?n~cmx#c+wq)2TjwNNW#nBl-*ThCCsnBuMV&|dkqSE z-{XXsru4I|{IG7KkX}Cin6t&w!p!Rd@jVYbO+y}n+Jm-+nLZVjOYAg^ zLbqh64NQ1+7_ROUZn%73{2WQI!$dJ9$U}JS@Ff@~{>(VXF*jJVV1(+-`EYZ+-JWzq z;Q3?1JZ3a#A@?NGqwEPVFB>oEU`=CQ79C7?i=*(~0tJmIWR`uDU>S&!!c_?v z*-71m8F!;h6Xmco2aCGh9TXjRz#0Ni%)-k)9P@~g%wpz@a~?h?6*t{c8a+GcvIO`z zFiT#?HFZRnvN6Y_x3j~ujr6GWQEW>(wQ(k-8Mmyl)gqm!{}C4HQJZD0k+gcG&grhC zUhGWA1~?#gWzZ4pyQ#gv7aQ;>R<6h2KWhnQF^@`1l8lHobqiv=17GM!7nwwjtco0phy?vqiXf%i|@)LuX7)rS{(Ylwn5Kf9N2;? zOkBcQ1#6oZ%Aa#4-$bfIsp*x-ru1k;5$p?FPI^!wv=p^MezPYqu||As;8{mDD73rH z==kZ^6F2fCo{kc~l+(6o9fW-`JQ$uBFe2xxR~C@*0npxh?#t=)^y9(6=~QviU#4bJ z*j7>AI(vx;w{QxoF}yiT`FYWH>_%05ZFPMHGy&+vV>v#z+fDG#%T0uH$TY(U?1J?% zp9>iYWXN zE**~9noMX0sv;{N)dBjyH18P)K_gNRQfkf9NV?s%Vt%j|jP#g<&m-Tv$WfO^6o=f@ zK2f2`Q~t{eG8en6uKGFJ7OH=hVvQ+EESZQXwR2!&W>gJYZ#>EO1%7b-rzKZ|72x|A z9NORMZQ=&#OA)>9S9^~+DgU4d*prKPX=6T-zNbk5LCCp-d>t%%_NKX&c`xYGIlNm~|zgRvbbeUvBFlpL&gMujxRIcK6abSty?ITvpysY-ls`a4#skgRI_BCJ|R z9V@r@4){vRDdYC@Q;cXthUvHR!Se^PWg@f&0EcVA-yqh>ikBYvEMoupEc9ZYkee|n z)yBsf2>MrK|9=1G7q7nsyg0{}2KVPI0%eUDjVgF;=rxm3u!qVBpfMhVwq13!%@ugj zjEMuFHi=f4yc+^q*Me5lCJtwF_DdH11%rsqg+hX3;X)kAZBGNkyp#O;#h^IFJ4H&r&Lykh56n}wVRh8>(%lzo*1vV;>8~n6vs7MD-lZLxp-i8T|i3fhw6pLL9fI7IyLg? z0~RONEZO=4;CXNOc4R1*v%3jli9)fN;thnwx}LwCe-6T(iHfv zp&bPCGeh!xSN-OD2+|xUZf36J;&jXoz-V3HSrGVV7Zw zBGKQ7sJ+XGad67slV9AD|JF}re;!{8;-L42^#4WzRVsfgn~#Ew3*|z>dkNrsDw6H0 z+I-FnLA2z!8c)S8wKOcFAV6pe122l3ewyAr#-qhr?1X~=gEJR1Q^IE%2ADXR+Cse6vl=N&chOL(R96fnEZ>Shnq@f z)DZ4QGKEc7PMtCpY_T40hI--f5Hf-_=fHdO=7g*ICriWSWt7<^9v{`7@w!!zxspIp zf%}T~0fAvFM^=?lqsh|7WHO6Lr%PDd=#bu77dJXLI!jLDtSVZ(^y4rVCvx9E3V-D$ z3@NTuNwf-c!c9y0Y4KlnQ zIu!VlVFNt47jnO0(U;7j;J6GD?9^eaF4jyF)8k}*-eUeUlBQbpKFJ$zEyDBeds2Ip;}gqApJqM#LYq|hir=)2g+iHWGZZKkiCzGMO*`Y>M_5?b zI2(e<62SG`v6VwIl$oFIN0-u$v8J?@v1fGa@l4BZqs(rw`0;L^8{Dg%#xJTD?RZXM zBO|6-h!*}wvcppLTg2Fda>i={uibiz+%Yq_8)h;_FXX@~6qzq7R9CBa3-~v&wthMo`l3Y@8}n40 z=J)p?N*3kyo05rY)wg5qTXwb3b$*x<$rfHE2-b1d+Cl0Fh}O|qoH|rw$=2;$*j&ob zy!3+tLe#sc0?*oWZ-3vP{t6*QpjmQ}qiA(o2=DP5Z!MSP)S@(^g{Y75K&I0)exG(l ze;DBz=Spn$wkp!#)#7?}B$O7n)WxOrLO+VcM zDQQ4e^nl+D1lyksGQLieoti^$UCImNfJj9o3>hkp&aD+VpOClm3v{J~E4Z*h>zGV} zYt7_3ArR8VH{8mO4di7YbBt}R!cN#%;ha5v&xKCB3_87Z@Z^t=Ev_IXRc@~%GiP=s ziE(baNyF)qFkq8`fRfFMK+G1N=2eX^(}ns^_PS$VvlX(t=C_zSiR8<^H}_t z7FRQh4(I}pif4Dm(5!9!vr2H7b?jEXAc{}M(EUIu=+bFj9&;S|K-HoHl@=->>-|U9 zYv{d}^WfM^;gQ#=!GDF@Xg8h(<^QX-OqQ?b1;I)&LlQEXbiVsi9GSPXCIh+WZgsm8(O)h}7gA*=Vh5 z^gQcx{JwP)cDMe!x%ZbjS0)lT8~afEcC!q|qVui}Z*(a{7BhvUajWk&g11E|mkk&0 zg58QgZ{kDRGAz-IziaQ_`k95~B5anz3X^%O9Z&mt*3%#=So_4k;o0Nz6G+=eXkWd- zAOYW`kQ#jXn}qE)WE20VI(&&(=;J|-T?qfGn{bsgXgtx$A|wjk=A(IvNwqO~0wq0d z9W^$55q?MC&CTmN^)qN^<)KV^3!Oq}OBH^#bZ)nA+WK2YE^JIU#;JUX6j2;++_E`# z)=0PmUl6oLF43e3Srtg$0An5){FS6)@Ct5&7t56{(h_Q;aZ zs9Cqh`{$z}1jY)BZHhnTXmz74br)_orr{1Yq}<9S9@cjqdkNLA#nfFN~= zr7T6uQ}(T78n0MP;93YLvUhk;Nw^LVQdWEfqu-n zT2Deh|7lS!s7oR1{&@OZ##tdEiEBIp0uRAK7FIMYqFrUK?VeprnzkkZ_CapxTL^0N z@+eR0(t)d8l`r5Xt&-hNMngVwE7)}qC-|{;RIQ&cqcU=@@+0DY2&X2w<+B`d_b0VW z=f+h3{7RoEmV4aFuSqRa#+@!o85uHhCfP2{BDx_uR$QeAUIwx;j(!IOSxsbM&#Aog zJotj^>P5e>A`|gpHbgF4=Ytv4ZOr0Qll%u45?Dpk5gV z$~==SKQbWk`F=mFH(_KF-3n+m*4Awu%s@AKSaIynKR1pfikN2C^guem`+{_8RPAn}y^uxz zRYGN8>)9H|=xLqrD4^f@lf%VfsxmSYhB&0DRc^6y7SdL$v*`uaVcEctp{YGG!eS)(dWeFO~E;>9U9g-&dYB6`17hF$cHIt}aIt1_T@q|<6h z0yXjVsL9aDBHqivU&I(T!{JDHTs&y*RA-7%#nsDFoneQjOUd7KSO=Y1H0A~WI?n+i zmOR0pI7euXnpu*b+OnnRi%yDz&F>VaNvbLgGAOzUS(cKx5*s6L6$k0^1E)uTIH%N4 zD|&PY`*jo^=O~gxM)h;07X2SbY4@?*LcKl*VU~-FxW&xZkHBxjzkS!CAJe2B?|f(Z z)WWb|TA!u|{N;JcZRB`|W0eW_L)K zoQKv0XdOmA7@m*0GDQh&`L#2BwXzBR8#@g0$dEF&n>sGqc5;FW{opH60Jv%}QVm5` zXvK^b``Up-FpE9LfR9}mmQG$l6;3ju1gVeBY!qtpni6CoyW1wPmHR!E)eaVTomwEF zkNW&6Q=~IHUxBUpDLJxW(|%Q-RXP|8>8|?s+(1Hkuso)WKO#Qej*0%w$8i0ASMqmM ziCT}-+`<$KZFd0Okz5EkYG%@@FJ{Z8RkBT3cgXgIS2?biieSfQ+z9iKZKZ&;^+GQq z;WI?rIBvJ7U)xbSd9P+`5OY7!(Dh`8CwLIY{`GGSaop$QL9SWY!Y}+kovj?6m45=p z3PU0t8I=h!R4{~sU!3)Qr$w)40g>OsgH5akSi*Aut#Lt2eeRr>z`yk($g$j)Ms)x@ zez+SX?PexF#!v~oj@r-)#-trH zxPq1V9iBs2g~KW~L}nwD%cDu5n)a_R3L2NQ;@qW0O1#$-6boUrX+F&f(&o|==KWI% zStgPYcN<-v9W;$e(GICx1-^f}%Di>kaTZf==|_997;70cDjH`m`>sM|vyYayY! zP<4bJ22t+*HqSz@7tMHC_b#Z~W}A1TtOJ|NzGwZkLyC}pQ$1Mscp37Yl*sl=WRNC7 z6@0$?@`g|79HkUZp{t_C@h#@Jk-{&=_mZ!z6@p0X;=I*%{=L6FGJV9+90&pS{Sw0|@=nxZ5{Lc5%mCfSN%tTjS5TctFsrIr|S75ddtk2sV6ah%%SS*Pi<|C@W?F9Tp!&7f(mh;E{K5lFjt01i{kwlI>#Sy&Q^}?r!MqL9 z_GpVTyXZUsIA4n3%k(!jA*KQ}hv0LE85<5Sj-c=JWlehDP0p~$@8B53tpmy>@e&+R zZRdWni4U7Rl%DA|mc9~9!Bkc)iQygopq464;GG)J=F9 zV+PaZg@qE=8c1iM@;))B(s9GVV+`TVd@3lEgj+{P;9P0!(3?&theB&6%_%(zcT=`j zF*;Bpnu_~mSMdEGdCr`d9I4M~#heA7z!e%@IfCJWmY2vYXI$xP-i2|LBn_%=_LO8t zp#d!Hze>CgqpS_hm6+S8ar^+Sp93BUVqDt+LBYvKUZ`eUfNe&-_ZEin8Q}b<^j3V< z8+IOw!I!?mN~njkAhK4;af$~~C5<^-K;s_>M3v4jxQ1%EjW|B^#*q#&`DPXMZxn3_ z)Nm2DOkz97Bqr})>`(YydwxIH$HW?^%ciIzPdXiFp^9CGU2vAnc_SLfg=4iG92GKAZ|26$h^K$S z>S28>#e+n*OR?hqgniQ`FT4orXk8)M*w*v)b~IRFIkZa<5;ZRR-smbs{6FToGgh&S zNvlF;9x<&NtoZK&dM~3WfmK^UiYMxGE#7SW~3Q8JrIqy0;f@M$=)cp*(p2#eS z((dg90`LvEb^LbUrG7RcO{RH;C-LtQihP| zyLI5CI?<%u2Nw-WhW{Y|SnvDSf77!V!Sx!A-pt#E3Yg1Z|vC9!ggh+mpYUAk+X86PC={;Z@I=wySF4* zcjn0sQ}7iHOybUcdy{&OE`T@Tb}s!FGhpM1oias_FmQ3*C=g3~7ciCyja1l+zTe3n zuF-}?23SB2G%QhgJ!QyIrLw^p7_{51vl${JwUkQ}xu?;WG=qscrCVefbv+*EMq>Q~ zzH44I7-#$MfQ}x?xAr2`V;aX=p!=9FlJOjxjyEpHMG!QC0WzOlLSuYt9@fr0pAhJy zDmwGO+de5f$v@||3Pn1zxe}mW8Hi0eXR@7XeLu6C#~KG#iTLE z?^4oIjN)sbYif+4r0DT{{I(L|H9PU|I--M-(f+ZG!0d`hCDBVId8j?g0u}Zr+&@C7 zlQZw+aYqK22jp@EK|skE}$F zHRC<4Ndi+Rl)rrQu@>;r>?w_W|2S@g3I;$$mp|izh}&I!c!7dmCe+?i$XLkWa8U~s zU?_?Nr!O^RV-5e0aqojTz9OUrjF}w2um~iatl4#DU;Z`oTa>g{*C9@q3PYV*V~<@B z{JvA^hcLHLFzW++3*#wUjiBhg=`Dr>6^3N{F;&oXovodh39vLxaXHGI)vv9*Vk`RAMlLML0p? z&)WdVWu*zH?_fPI`&nvbMX&(g=$t| z-zY@%U@ioMX%u0cCG`vnj%4&dXJb1pW-=2~y&KU#g__%t%$ydYN6Uz)!WT!Pgz(4R z4RUe>2obE+SSJ2;yfF$G0MSUo8dCyHW6x+{M`6vHS}nVF0{+JBKIcl5U!#J9TmDu| zbXv2Q?hV>OBhm1^ueId?8-~)NcnK!k=8`$7=XjuhRY;Hx2a`^V9W{=URd0X(zOAWM zHIpthrpaG%n6u&M@d~E(ApX&;%w@!}c9WqAbPge=+FBa_W8Efl5S+{gV_XCLp2%2C zKS@~}5`R5&l=fTaluk>xk^-hC_r1)2E#w8P|BQEKcx+8OPMS4Xuh*?@@lGo(ciad% zG4#5!_5tFlT1OU;7aTM>N9^3r)*dy`l%wBbTvQI_teD_!Gk0X0g1}JS|IzE@p1!a2 zDCf@eYFlJM12g+BjL}y!nz+>`>Vy+!xe2ptf~v`B@P?aZ1+m#-$|L0C{_KjeR$Zh2 zw#X?I*9_9IbFh|7k7G0K>P#D_jzr69{QW7uQNnj*RRG5PDytRcAADz%m6P{PmO9cX zg|nEDMvR0`^!4YqaC@l8?@ATYg+DsoEydz1 z89gnJP@O&?Q4V`w_{cOJD3j??Rz_5AWOFeSj zha?v0BgT-u5-%aH$vXH1L1qT%50Tq|b({5KO)8R2KQLR> zL{4e@4Te60E`5UA9vjxTr0c;(oL8@#{Wq(pkytYQI3u`b__=D1)LScI)eUeEU+aR40#YbmtbbWhXGG~f%hXl@9+Hcl|lwcjHo2({@S)&#)Zcf z9OEYsulFG=4@XJDA<8F^9xT_~<5fhz%#<3h**8)h!7zd*3u zsqH^blOFnwAW;qrkQEIOGI-ZlMoTz;PTZ$KwjKaw9mY_(yes<_nA`>&` zb`M2jy}9v7f5OhYh2iTDKk)LWn`K{!B?d1$#i9tIf-H&>H(LEuk#0c~pYc`IXT2?g z6RW;dj({>P#eCRk9y;gunN0BOW!LVI&2yc(kDQ7rUxKCoCx`&&eV@BTr4>Tl=758L z0@s9)0Yb@CsGkv~@h+ay(9+^d*qc2j9ondHjIFshn*SM>##-xIvm0PoTO$b{FZgo= zN>(ne<*pe0LX)0A@qIX+_Fa3UBtDqYe``wsw-|Ke!MDcAjJ*dnJ6j~Ke z;s(%aO3RaH?6TVC_*^D>z$jWh#vQ5-+AJnTmR6EtRkQyF!3&{z$iO2Sc${g2XRDfa z>GBZ%3k|;r^YC7bmdb?al*l#2P^z_4j+p8!srS!knBHGBY~FHZ$ypMz)}>Rr&#XV# z;(~UhBjFJ7Lru*N<{Iza{-pBULYs7-S5=?CNDO1L=EiV%>dpKpRxVHiRr3x*T8%0%|q~~W8RHfGmv13etg03nA+$#U4D(Yap%{guwU7E zgf4rpn0zUApk|_HAQ&YQ-JF=8PCXsl_PyD9q85*QnqW4U4vAN}PEenZGe`b~>~<$|!3+ej(J z-5sInXdZCRS(X(B{8Hh}EKK*8x&8CuV^xlv%3+#d8Rhe1y<+yHvg3O*ax0HTLsn#$ zpxC-*wkoqvG3wo`U=b2BGBS_=504iXWdzZN$fSGEWGWNGqfY~j*q`Pa0;=mv|11PT ziXS>k8<#-lN>pGym;FH35qaL{iW&V)ZRL8HnV3jSe}4dz{(y!6P8e@@Ect^e z3hx93ETZ#_ogwxNY6-GXk|ncT8T(RwdwkZ*Iz$VTgJFq4#lg*;#IG{God)fS&&Z=o z%T!ap@38}RZ>aX>YDtI4oXX?cdrh0FH_s?!(n4(5KgUT`OM)iXRHMUQ{YNbTR_gth z?Ww5q8>1Q)e}GJrzx?(+a$144r-Y(navU~rBB@BA1}9agDp|9HTtgQsZr>))JA6eE znWA8KkQ^-VNio>nlOvE)Sx)+K_2Vy@qg%s|Pc#hAfhW-8-a)@17vB*x;xju4bN37b z-+oV3X%}ABN4|gqVf6|-!IcGxFgP$!7PL%H0tg^BM|R_Upn|XMmk~gS8+-~u7uEIn zrl2j&*07ECU4CSGDAVix`9#{l1`J%?6$%6DKDd?qeAP~ zhJAId+maX|*Uwn+A9=s%(sH+L`&@`GsBgLNdY`P*Cq(A|OWKslSdG&02gjR-z^!>5 zg_+`3wDCQNKW_W7?Z5`KI12<)ab~_ywh9uhgzXeuLM|Bh+^KRW6~w4apVu*YJJ?46 zb3{L0e=X6-(W6ia;0ZmvCdeORV@Li`JSd~B)}X9j0yULf&UX}M|E0wH;DupFw$2ZD zAw+dfEYT|fXj?m;pQS_%6`ojS-tX&Lg1N3GWQa`?o3Y&0#oYI&^d76_t>7mtll}R} zbI#fq!N`Tfod!E6{cPOJ=qjZnrv6)cUCJSsZtK2aX;caD?I zRE@G%Q(awMh=GO%_U4B*MK~BS zN=?-mEcFxkKlX#*E}WksVkj`MJi$hGuUyPl7HJ8pb<&Mce1)t)q z5e-TsAjEmqJ#bMOZXOHA6imlzLw8S>Rt$^F&L;%sBj-80Se0v~L?J9;yLn~-C&)PC zLGaZ-Zdki-#o=a-?-@F5Pl!z zewL6wE?!P?_P?lYUnkAj0V&(yTK~*6`lIM?5#F}pER@vv|0)+n?&)DcMdOk1%=1G2 z*2Et%jeiDm;^4`Q*M++j4)LgT@6HTTd^IWwDCUK}Z?soH?XcG+mL_pl%xtld(`yQ; z0a84&#r!KrmpcS(xGz$~zdi%cdepuT8xpxiskDU{#5ishHkQ#_VkHPN=Hlku|Kp)- zmXI(oYE%ja=}GC*{^g=;B2tKlh&W8i018^0L2Q%>BID4k{yTtIZLUI%o!XCv<0$i+ ze!Ni-TpW{xqQsYy&V`Jqk355&`pdqDTdh7VSYG#&zn#iR-p)r77hb{tvdn6gzSkl3!aYlr%|{>mFg2p;QY{)Uq^HX zwu_A+&C$7pWGEh{KWQOfWfbFCeK4LXya9QWziB>at#nJaH$wl8DK*SfJvIF~c)(?=qEN3gu)J5&re2u3XYc7k>j|TRId?38{l1YKo5Piji&H z+O&1CB*-w>sN$V076emMO;CEZAR{!bm=C)^T%8&d)^+e=nffjn17=!m7(K|}LD!b? zeOZu%H5#-d?e);NA5Gj6oi)phdzT5cf-^WB{ez2uwFf;pbnSGynfN4wJ|_tddk@gy zTiy8Fo15z>w|@BjP5cQkKHk9ew9gG?-;_?TXL7E4o+6`A4AhtfWW$$&b^FVN11!-3 zm-WMzU-PZ1zc2zCvl4{wWe9_YdXCzTC(B?1GHw0-J#YnNpl?$E~!CXM^AeJHBI2L5Ko_ zTsiFAn4ZRPg4#)hg)>==QRxe28uT@A+=2~HT79C|6Pt%1PZv8JWK@voyP@9L6{NzZ>v zbrq)-fVcy8soR6V-Dj#y!K7oBhqN}5mV!zBqs&V=V?La|)5=Os4 z?a@g7;*yX8ArS$K#k^y~(3>~>-EUmftc|Iz!#vb)ouz$qz^{!qq|*A}$KUG(j3>3< z>|ATdArQr-GqVLH-qIDR2;R&*dKz zu(IdqbQdnui6})Di~7q}m70KR!={G3T8k+K6Jz}1au0zenCM@sL8LkU*;Lt{h1Z3r zKLW)tD6uGa$$yv3(x*`wVn5NGK~GcDE#$N;Idd(;X;pl?{Ff(sGYm|6z#g&*fgnlV zrw9@^*-R*X+NaWeEb2K|4;-1lCc*B8K%+{C9K$;9{E5b&KZP$8k37%yP5!R0m^OPa zKNmFF_)CcNSFDO1D6=Cxg9gS?v2-H+?@Zos%iSpGz$N^j3a+xo633FSAM`-jyI@*)@gZPnvU(glJ(AhjXy%b%&k2CPis}R%NBuPBM4(n>- zL3r|RxI6mJf~W({Vh>uIl9JlkZgR*q`d<^WHaW4;0NodLU9X6bY)bcJe8Gp(_p@B6 zqf=ahND;?G*DrWph>>F~RH-=jT)VR?9zpyP`vRH9z6js+%}nl1EvVE}_~jZ*ECR+n zLb)srkMdR$wZcQ6mZBMs3P89edLNw}7gEg`90*Hd>ASn(zGC*$B7^9sh(n|g9)vM7 z?~uwA*odU!*N$(u88!JG0%m!?{-~?~uxGgL9d-6IJiw0#oyeI6wmF5hF8!eJ|D^+sgqe0?;$y6?53wz;)s{(;*(D_+4f$iT_jPg^a8joa>qj7y z-`$^VjhW&Dw(y5TFt6Vr9%?srLrsxv{?H7#p7u&g3tMCQO(I1daqKJ zgm$A?Y((B^iGgO}(DZ6F+Qo{tvrxzJ7nIi}fKq=ZwxGkyj&cgOpX`52H4^d1xD17A z<=3_XDCNQV($9d|8CN8=l9C|Y1{m^|Efv@P!caoY0L_rNk;=%5KHgU>ln-2)q4e}2 z_56Y9aWzZXv`cy=nBk|Vr-};w{e5be&+@CKoR&B?II1bVzF>qFiI~r(pW`4zohu~& zAMr$5S1Z3p+3+O!JF`+U0brzXx`}WVQSk?Ct9CoWi9rW)34Cg9xqed)&hBx&Zv3!E z)4;1O5GoJ1W%CzM(vN5-%hC*epmQ&xR$t{9u6@m#`1;yy_x7qj@a^JDWaW&;7E5V*H_jLYrB%_NPth9{8 zTXu7{F6!US(Pccnr$C!JbvMC`*BxBTm@&qZg+n@%^Jm3ejakE6+ESQX*3vyWz)(9= zVI*mn30*SNO!g;qOthT8YEE=eIEAhRYE0ftwc@Y(bYEu}Jn58wve~l6((U79mAWEo zqiPF7Luk>$*wyQ|%HtCexuZ(sK3p~bA`}`q&J=zv)3#K5QAy(@I;?B-z^vdyaGAjQ z_9G@T>5~gxnC>w36=V9vRzC5f5pYC5bF3;~^{#}w*?wKqbId)2x(_KM%koYx& z!4G zh|#qoX)WYM-YBVF7-m%stnZ=-?`Z1)v}!Yq)-iGT;pwY(YdP z>y&*v+2KGS*;MM(&KqRiih=sNH$R=rhfbDU?7qQlhy)pDC2*XQJ*R*S@>4K=?k~!; zB=w1sTaO(@$|w7o%*+|dyw3enQMoAH&+UgqmWB_D!I_l1S$SMu%qcQMGb!H)GZ?!> zT*cCL^P`KCqKlp?meX|%3KiBAKw=KyLUxkTJh$g$tpaTb^qv;QWQ)Efx(Hj+O0Gm({hQ z-fmJaul%3s41th86_L7_Y)Ho?qQsF!+0}ABEasVN#bq0SYg1A0FNr_Uz{dln^+i2k za>h9flW!jg9%_>K4aQ20V-7SjUr`ql*;)wKm?jnPXgS95$k_?+&H%2LGD(2rv7eu3KHhOp3`EiSu%6K;+23a^*{Jkn=+yCLH70o zZP=mSF-inVJwmN;B+jVR2%%J+1pg=kx{sa71nOXq^w{?I<76X>Jb98byT0YT_PzXZ zO=?GZH~nDfNi-3v0WOJF^Lqp&Ww`I*m1RmhW;4<}Nc;P$lgt|w5s7QNZ+JSJu_nC^ zRSXj{@MEtexO@n$f5E)t2t9Mvcds%PQ-TzMa42O-6PL;j%t*>l_^i8hXxt^yr1(da zxD;yui{YC%azSGdD0T2GU3iWLJj&61pSpB%0hGcz_r7^^bZTgV3VSi zkV#k2cu?EHNra&*O9&Jdx}%V7R?`rH>pmB?n2Pq?GXrW&>Mo77Hnc^3Eb1yBdd{qp zalU~#U>{S|1#y_q_V{DDy>}zoHM(XV+F*q1|9X>BGj9G#3O!Bj(bQn5OQH&@v;U83 zO#3WIX06LEUrLYs z4^5@glU%sX@Xe)$hM;j7x~TK?KO9h>(Cn3qTPTPNWQSTeQ6p`KwH{ zw$So0PT+oSV{SO4#NDd=bQ*3H&*7GffEEpPFP5u{5)T-NjXFu;+xdi!0F^q!Y~0;} z5?3rMvPHE&6NNpf-j7g;!^22=zA77OCC;tzbBV7FMnREvXBJs35D|kHE?PsxV#K}Z zzQV(-mxeo1!f~$P)zH4?4$1p18b-V&OcVR3vVYna%K^l`;u0^50kxgNaNfEo$qpFj0qXx%2W9-BJNg0yj9xA2GVcZ{gV-tM1e?a&jzTGxM1cUtk z_F%W*O2ypLm(WiKf&GsOf9sE20=WI!oKuW?+hR$|rur1rT3~n&4p4Ra>jU3`fX^NN zmD8pTnk~0m{F7Iza|#w`Xo{H48A30S*j`PsHhKU?1)R8-cidHTiY{hopnMNTiukVM z9rI+KLCVxuJ$_2YJ0(|cB}&^KG5!XF91=RA0oZt3r*?`y=r%JPy(95N;5IK7CtjVy zx|sZ>uA=R%GKN6tEA@=`X-<$tfgucn!3_>gc~nb_KqCo9gNq}a@NcoBGcXcs*9O^L zWUuTZZF=-Y@{@XaVJuFo^kard6J$i4P-NJ}0LDvA_ghG!K+pOuI$!r*_=VG}KyLZ& zHdrZUA8MaXM{@h81#E;K*sY8ip{qwm#e3L_X~|0aWqh9oVkKHU$rddYmc^Y5D@Z;Y zU@RVXHIE%97aA5n(p~AUvdT$h16s2n5quKM6BGsDYCz1NJ5_e05*y=+{cks0>tKxx zRB-9Wt)@QurC>1FfYSFGlj}0^bJ%s$V zfS)o34lJ@GIlAE~7X1AoP7O_H&^KO?8U|7}dG;x%bUoao15db0~+k=P(&5 zleGz+#(a@pe-If@na_=C58ECmK7J25wNp!~md9eaQ;=^1>^W_|Kj?udV{(!hg1-FY zsz~k(OV{7yzqonBZy4+VFiE|iCA-)3hZKQZ@-5FCO+?r!w$Ma`ld(BtDMl& z6uTuHV{CO*Q8em9lYUwky_GAKfl!999A3xQ@tA;_{?2hoRqJOV1hFPTMe-h#&h%*D z`(K>yAg+E3)_QSiN@o#zCNl?on**ge<|%ZmQN|%^(#Jcnae^8|xm3t`gZ@<25pHNZD45`cN7(a;0G|gVJ1|@?9_NaRXlM>` zBWSzLnP6#M0K8|@ef%veOZ#6u*i`)e7aPqWt72rkSc6{LvFhKf!50XLzOWJkr(L7GAM964ni z7_LF9qzT0UZJOj07lf|VnrUFy}_fbUq;6=G)&yMyJn0(^vx61t1nS=P1rwgpK=sGStK5%kR zE0LY8`!;xRV9$KpZIY!=oJKM#=9^!>Az`;5hr<1orbsaP>mVJ6@hzmEt?~l8;B7&@ z-!l`DS}r8jxQ#>!%!HmJyT~GDzZPgm8zDpbP=P6TkGx@b{FzF z3ZG-TSBo%?U)6C|Q4Kv$=RD!u3ejgi4t<)U%lE^YX}jn{c*#N!$$in>u(&Z(E#TI@ z(+yeEE~qhi)&MA!OTmoZ!w_e2DKfXyyH^sUcjJ7U2PHZbhK^V4w5s=%uF#KT_1UI4{L&>sIW*JoqXR|a@NbHVrW)fF(6 zDdML+lh30f-o)@)#brb_V?R0*@?Jt4UA|mpHs z9U|R&amq^AyDC3nKy(0_8wZ23%K#!>qSy*7OUiOZ^p$&4jB;byn4IUN%%_(*r22Fa z04roQd`R30xd!9uh9EC-rYTz{(G-0gf&i{u*Y%&{ z*6h*kTb@jM9F{gLMlUOY_jG~sj>Hp4K?Z)>Kq@;g9Fy=J;0k03Qet*26>C}uR@g4L zu9$SX1ypt#Nu$=v64#xZGLP)S;P`q%zysAX{+DF6={w{hi(937$YLq`rMui`D$JDj= zBzF80h?LKuL*ZrViFu`TRM1kI}cHT+rUA1Ho@@?}0BBz*h$+5`r>G+};52Xh^ zDP@!+=syX>#3Sl*gbzw|O~fxN%-J%@59G4H`$~=FTA+YXff=QiIT01!JNSb8hBB>@A zTJa4mfE>E;K)-$08-`Toc1~-%w2eZQ5_-PfqMa8OTh%U!CO3lnF=-0{@YJmb<{wK2 zgt%9n;`Qy&*+7xsOu%!IL(~QAv9?QY6O?3V&HSOojzz^6{@!=F7GS=FBlGby(Jp)v zmRo8^DxkJo+UImsiFYLJg(HlCCkpvh7J?!@01S)2f)+b}yD$u8WB!J>pj9Iw2AyRR z=Ve-$^s^rdVma#Zg(}8lD#zv&H}F84)06gF*Lboco}s`(p@Ca9OFQL*`yqPbrdcBHYpV01GiS3EV2Ywj?bdGA z8TU)rF`ZnSVUj2Fp_?bdBaz9P&xO;fUFu`XxVPxjZb;N%%4&_y>Zeqdpg$+(Xii$0 zzMT4{ub0G|1(Z@6P@{f(@>>P zg=sQXLhDBj_ZKNCcu;=QQ2h5SbC=d}(u-!wRUo7GHxUUAL@c%oAB7>d%+HMOGgwa< zMYu)TW;9EqNLFHUv7-!sdpou@v>)aoV%8(OYtbP zaQAfKD}X$Ky_{xJ6ut!j(zMlt5+(T3@g&?SQ}6D@TvC_sh1jJt7a49mG%?vA+g7`! z>z+qr0_Anb;xDj&U*XLrKUWD#Gzb#5`L7D!JK{13=E~0ThRVJ3uoF}ndMEskP%yBZ zbA9uDqpl-wY|Q;xRWjZjOt-Nnhm;9&dFV%Z6Po(oFn#L~7MVk|^&IH4@@^5wd$x*a;OGh_ z-(){o{!EOKv`S%@J)Yem(>`$jDe`#vg~eHd#OF`9Ey>9BXF>As28k;9VeC=D)K#-{ zH)n(zN{pmT!?~=&BZn@{zI0^WyvQZr029i~VmO-KYu1MAE5EPKzbX4=d?9mK0u2dY z+k({SGt5Ke3>f!mO{2!mSlO;*qUE(mMMr*X_8Y(7r?;oGp#B%P!pp$0!uPWhK8D*! zM7?rHS;EV2o8VcH4NR8(o|<_V7YvFP)F>TN;$4FQ`P#E!NJbdbW18FY0*2R`tB8L? z-|{d#qrU!K1Y1^rd|kJr!%!8V8Cz) zO!9#-t@!%517$QaX=;{<@S{`}GFoWjc%I#fIX}U0I`Sdh+v-Q?SrNfrrRJc-PYl|$ z#L2o;sAno<$*266Gkocvq!_%+J1*b=JqYhB)v%PXJ^{}^SdXv|m}}amvf&GL1NMTN z?@Gq}IVcY`sYjuIK%Zci*j@HnQfnDEx6?+?#5~KlyMEN6pUv#U0?{CiGjw^U3clf3 z$5zqdWbchEUQuOi9bAJs$4}hO63$aggK=Ld;{|Zk);ulDW;o3pe zI6^-6<8gi4k#eU4x+afeipPCEuexrowG9ryc1F|1Ix2XoPZ7IVJpF7E(!J}<1dfv`vn z+AYj7WLN!j7~yv?p-A{1AqVA)LJk=)K2f|DihQ}lzxhj9x9W(LJ$+wB|2%*72p$cy zcFeHN>UNdGiHTHpwTSMb)cx~{p?+filt4L)q)}1E#42K0DFF2ZZL^y*QZnAuE1wDx z&-rY=G`i9n%EKfZV(yRM=<)%yv#9>=YB1P{5st`6y{4=0%Zh)7|%kuNDG%?eXMBF1(%_!D2CapVl4E( zp!s0;gay4|T4V8e$;=+?zBh*Tc*GkVjahJ)%HlWSF;sgcV;&c@6}?Q;Ooe_gwXAKN zVd^aetE0Uhkeo}`U$R33pJBoGC3U)SrA@9#@msAs56`h@r821`P%IA2{!?Y8#!(r1 zf?9mvbPlr@K{y~wT%p)jM7reblU&4Bw9^r+h!a^pEmK_7JhR=O7K2kPHty<&XlJ1w z&TeWWyG+$}%g1@gPCv%-ag`IS@tw5RqzTR~oLIH~`C#nbL959$%wxt4t8}BpqSB8n z8sjI9C8@p8AABGTQq(d~*|5Fl<}I6ZQi~u!5n;ddzC=Pcd39g1$L2L zBsccfYye<{nEpD15B7T7eL*Ql4?Oz*Fu!38I^=IE)tIlt>$SADHn0X|hEf>G(hY(# z!9RDiGH5O?uD??R*4D`!egLkacd%R_>i5*e#JSPr(|r8hwv&pE$Bm+l)sb(e7O$?a zaxiL-U_B)U0t^K;6bKlEiS|)V>SV{FuaLPk3buO4)AJuPrmAD`R<5Hml80^YFXykH zMf%PFu@pbP@oD@C+lg7O{-p>?ZjiNby>Ha+;{rT=i@H>^!Hg$ zF8zK+4jMPb78{DoNwxyf8vbDV zAaA(iBem8!D{sVWxY-5(a8(oDI!5b+rE_hnD_ZXBTC!3SQWI&d;c6QK0s;`s02uT} zLKEtZ@<=Ax)&8sZkr(B5<(X;HZ1*{7*L{k-7yAe2GOgY;%u1WZ_e{qm&^Wm*5CRYh zpmr2i4bf;f{S>cix&cbpE{L8Y_zw`>{eT{1F%vfM>UECP{&ai-0;`O|;~$+IQ91B`K9&~Do&tki%9I_b?^1wMp#vVn1+m0)=#P$lvXoXZlsO&#TVSJ?P48kYkA1MP2Du3!LI=j3cf8r z8K+9MlX+lo;&PjI7Ao1-P1j>GhICR*oK9rr-g)~7Vooh6FK5itIv50hn8Q%Xy^^2S z+-EKM5a}G^*yt)$>W*Cf2&w~An5q#JYCX{;$>Bs;rCwb>@RSz6aA6qv#FLE$1z%SC zvYhh`gYyeV1t~K}^?#v9#GOQECk9&je}hHRH=aaFq9vln8dcp>ttutH#;Vi{$6Yy@btO`Xwu=+=IPZ0<`a!TOOZF~!ZOswxHQmxIPoz6!3CxK$GJaIT6{3(0Na z^WzlwP@pAgNK${I2NK_cFxzcJjng#q&(;38_HK&qovhUW*dlyB0LYF#+xp*n;R zwpdxMCn49kG|(OaRjCc%0xoKr(F`Dk?tz}8SG*UzFi7R*uoVWMdw%=g^sIm{97+rU z8q|j#qrlo3+7n#j6y^72{iZQ3s_iT`yd8r@Ts|yz(AvDI6jRljc{PS@?hpKuW%(Hy zj1z-I&@P+sGm?qjUALYeXJ!UwRmoVFw`f0Pw+A;CHMG_Kit2rQ@r9kAJiJcVFDfz$ zaR3j+oTZYJA>c6_&~=6D!IHBrBZflx7h&?7b-Hws%tI!`#Mo6SMD5?=j+G>!VCZ0t zq>zM+b*{%U*9?D1;m19qT|K^`OFwu+GV!ES792#i+$pSgCaS=qTiFP96b!f<#xF$u z?^kQu6y$9P4TsWD&?X)ZF9ethn#}kJq2QAS*DGT8=|9336Mo^vV;#j%%!2T#^KaEl?l_W5k zP%O3R9BDi`$i%x+)IW2PQ1-Pk$^AxZ&Mv8eSelIC>qtFQK|{$X&$8~I<0l%M>NbOB z){uGfQmL=>8l>I<&a0$>uXp`$&>$ej01&|LZGdFrwFm22?o!=VTifF@4a2SDwKagu z=C_oplZ0-~s+s{Q;(4h%IjjulN!3#lMoTh7sZvvO)y@fLI7k;@hfdU?IB?>7+Nc}s zHlaJNwi{qR)J1_17+hbp9S4sV_lFfzc|aNpDB1{GI0&DAj=?x2GV&ALbBCaHSFgk! zu=A4Y=aJx!7uQ&udY2d@g)K<=g|4cLj2t-P;?zF2mZoQ&35I85aGx^4L1MzUeHCiO z%rHCWoV#-d$%=T|Q``%RcpupRDJ(i6D9~=`;@dko469AMuT^=@CmOjR0Qa)zar+W? zio|@rM6Nu~hmjbwb7Jqg=aS9c$h*)`AS-cqw^?2kC78*cL9AkCN#l5trKps z=sWYQgh8I93Pu4eP%_<}N8jW{e5_4&{hxVAkVL|3|TKKA# z2OhEf#_v|aw*xnrmD+AHHq~_1sA4dZpxKf9>z^zr_ z4{}U-1=x6-(+cHc%ghA&y|>1 z&8K-*{0`z)gaZ7)E7gCx8n7Dr+D~KXfXtIJKSmUZI&MMb)0+wSs0g;|-68dU`m^!b zj(5=X$R3s(2MS+cb@`kEAKX#86!tp2Z%ydO=AZ2ez@xQ!ToZoiU$Q)hyq z1G#JAM)}R+jwqWI?zkdhLZiETBexKh{vK5SERMb}wAI}25rv6DvPQMqTiW&5?dI?b zsN|AW4~Pm6pV>!P=4Wo`gZg&~nT$z#Tp!bXJM4J@+ukR&Pho!)Q2OZ@D*^e@*tCj? ze~iGb`9JSQQMy6dZp|wFXg*ZXn36uTRKuOz%3=>of%y)6F8OL1GG|bp<2I4=WoAtu z5+AtzT`^k{su_D8noB&Z-*R&kD4V1==1*+?s) z_$d^C$4UpGs#p$LX#Wyq@`ijjhr1~Hbr5PsfMNstcP>+H-H}kZ_^iCFHY&wZn)zJ7 zvT&H#wOM0v0;?WajAAXr3%-S1FhH2`Q(ctNJMa6V*6L4)nw$VIB+(1DbQtFfG)K<- zU#)Ft@+Y~DPqR$JM41$^R`-~vj2iv=mk;m#=tgSM+e*Yp>!23Lt$&JJIpAu^fE4kw zY9j>OEnVNa=p_OEbVRCkRR;;gP9n`9sQJCQT29$m-h)%DI!pt zW}mvsOXPOo$6KaoQ8?K3=j&J^g<@(!HvO(iR@?wQfV`PXCf=_v3ohh;3-Rs=*p=rs z84qC-$Hs{+bMGq?Fyio`w#MrT+Chvw;Av02SoqJXg@JP?4Kea`C@B#2XK*j2WSlo# zl{mJ$NdB%Wr6&~c_nTsh=hO`R&{bVu?|_b8Tij694Pj+C*(@p=CdfIwaB)$%^n=I= zb$6>bRxzpMhO{31Sv6|WgfvU+fT!?%m9ca7Q0noqt`|Wk(r0D@4D7I;vKxHA&s5a8 zMjccmqwC;GQ&h-!-Kqogy{$H`+K#+0Q4rg(Z^Pq)8~uM;=1;Vi zI6aRgtxlIhUE8B#))+wU!S@KJ?Dx0w&96KDJK+BMO3^X@{r9o~SY~6#sbcWS{}TTdz!Xnw#&)ihg%{FC17>xH+fx5PZVc-R$n;skW-z^2kBft#! zq%`nmDDiEjNNHR-T^Ul-;I%9}4q9j6>KG<@Kei-}bMw>K76HZFW(&kf2L2rT{=QDN z%@b#AXKwaVfv-!oP?VJx0VdL`oMQt;QyC{YY4Dk-3+Z(jqp+|gM$}Y$BbH5jEPxq`&pTRRjQU_?KgC5;(s=MO!eJOYl&DDPUaG2hsWj6j6Ox?2zznpHvTAw z3y^)rFuv-WeGH)z-zza*29icOl7WOH43-8s0*4TJo)?zexKO3d2O`FXgSS^Sd&K`= zb8i`y<=RD!f^?^pbhmVOcXvricZZ}jf|R6E0@B@$gh+QwcXtctV!wNTzt1@1`*Ftb zm&J2G_Z4%^IoDjpE!){Y4oQBfW;!669{a6kmzrk>Z(Tgo7vjItO`w7ha+29>Oq1jv z1B1oRj~Z5vX`zZE-q=+_^5@2=sad$aO^4{TK@yTFA}98{9M`}OVAXiJc>ckbxJF(< zO6*C9Oewro9A-J#O@sldX0g1V1+VAOgdZyp_oHf^K+o~!L=+NZdDcD0C5EA>cVKm( z=p8Jbi6`AHY@7)RR8~DzzYH2?OT4}8tCUw?a`4bM+qIMv{`fiBQrDyk`z(;dy9=zk zP?w?F(&D@gJT8h2d{zyL;K5dRGYA^=gx=9rjc-b8qj{%GDn~CkB-%1IY zB%;#`saaE^4_KXZea9}Vz=^1ib_C{CprB5Op583Ejsm0FFX^xD$jvu3h`zCkU{p!V zadXMB{0p>yxiD90LU(V#CBO4xh&vaQ$51Fsfn*LYy@+c+0y~~tSq)HDgvi$D$~jM8D|tK1V*)!u8dn!1Sm_}ey9aOH+7R&Gr;q;VT@T)Z)05yuB&*yq zB}OA;5_65j{_^`H;muJjQ?0`M6`9(u{n2Q4+O@*byWE`BRXrHVfr}cU$yezQ(Nh== zB9~Hwf3$l>};t;S(vff`Fi}%TF^MYRfhvSwC3b2m;p%vaiwu5*U zB<4orE*M<9{lzH*>quGcI4y$*y+;29hnAy$8h*kEMqi&Brn)IL%8=J9=@#YX(4$ys zYfVRTg_t;vkZ^?;JQ4{I*@VJwJ`*4K$Qh*lNG0`DMlk~~6Aa)Mwy^k;xY{x78Y$y^ zVIhL?<+_zO3r3*ym?gKjHtAf(#3OFLOL2ev!?yPt**6~d5K6?ar}d0mtqbHW{CbP7 znH_o#290q8U*MVD--oN6ZYEh6Uv6&J$VzL_AifMND^Gw+@c=L7AIN-an}C=F{%PHe zo^5eaflWCC%$^l2wZnYaJ$W?aPi)_}>0QbfQiQQ0iQU8o5QB&WC| zz$8`v#P-dBHm>Kg2nmnzt3{hw7mBHfNDXV+80}w+>buA=i-Z14PndmM0WuhatQfNV z!zeZr_>mK+tz%Rb@MPfvnDeSX{#rqKC^`~qvNLrG7j=oEmi9fIftqLRwSQ@IM# zvhzgYv6{=m@3Ga4+iIyC))U|;t~%>`Q}ZUgJ3iLM4;GuwqR@U8s&$>`*4EZa3;r9d z^5!pu`!4v(=K&!e9_kcRSmJN5W#%lR-prYGTE0{Hs-|B^U4rY^7mkTNS#cUt=&1Oi zlqdpKPdeo?aD5kBl0x#U@TIhDmAv_nesqZ2h-@?*5)7s?rrNg_^~1m{&G2wzgPM)I zo1bFDXWoxa@6YraLS~6`APjkLEt`FarTaxMRrnSQc0Rlq9z3-FA2qUB9hz@m>x?x+ zKRMC0!cKL6vyh8mptbWteEAH%I&wUV)TA8(``1*d43i1kx>*~zfIFt2Lr^yAeXS|O0 z@|s9$#InbtJw+H=!yjfXs<^{D_KJmMF3;fUyeYDIo7*>RpAr6d|BH(qg7$EkHglqpw9Y@lJ%oR1*YXEz+8fX48C}b7 z091X!e5Y%A4Xzt7oGu1Aom=a7`0LTb=mPgGv_ZxigevS%1JWj|n z6LH#L>*C)`J#cYC7OH=-`PejP(Ml(N44GYJsjakrIO9POvCUKykfc_YlC8p)Y@v%gBS(kE$g`i?>K`cFh)+;VW$Ah> z831RucAQ3Bi$>XNokN>@eTnk93Uaau%Yz;)VP7!x{)4#~0 z7LsxFU-*oO@MGJ^^6g`aBV@8pT>9)Kz?cuR_a#~`Mrq^<%U!NS=UVMP1>FXUGwfWwNP9V=9lJkV8S)^No!R*ll5A(GzUl0QMXrx%QN?{5fv&4L;C5JQj)6GfDXqNU4IHgtu2 zj=hUdJx$}S4co1;pU)^PGmAGXgVuOJKIoGYO5-Ro@$Wzg8+5E4mjd*t^z8oF@#}~L z#B)QmZhplvtCa*QPyotBDFnau|>!}tsExI-2T zQ=u*`rR2M|Dqtac1B6IQdA?R`{Q7KJbO$m7=G)+~J-?N=-|yQ56*>!?-NDXL4VN(! z|7LS1h>Ifb{qGUTdox3I#KTeRHaiD0eZ+mJtaPs_<37B{p98!9VSu56r}VAk&}{r?urOW3N$kz(K}i>>!mHVX92)7kg*NoP7bO!ktN7 zD)ZN!?5m9Y4!xK{u0|qt5+>p^;@_^XpUb-Uk0jf%@MH4(?>jZ*#fN+5xIS%X1w4UH zu^ZqT`hj7kOKfGVyVr~RPQbV`kiNm3!T5{<;@Y5^_w)V3?3Qv@uHG2P%^cfO56o`MJvl zW6>xbl0Gg#Lbj*Oyj{v&_go>ADACu5l(T0LqM0M|1ECA8V4?-lf7>6rd5zl@l-Fn^ zLQD4nr4bzV5?zMgE@H&WLB=z>7JtkW6#fEqc?Pg(DOD~a7BL%Gn^K~>DenB_{YeukZblBe?~>Vsxr z&tGv?xVKLfp2ZaIJ|n6_C7kxP?C&g5kPjui3U9+}EG&9>V;5%$+qJ zS_($c?^ga+bUdZ=%K^~L{RY)uIjRCBCPvH_?lx(a3rDkyDp@DkL${*+-sUr+1mFt) zIj-S9Jf8h}p+kYWaOZXWeTmY!)IQYG;Ew*SjqE!>@oGtXc3D0A0Jd!qk!)lV`g+d1 zpEvFs7pF7Va9!f~mOb%9ViZ!`Zs`RjR=nnM>uee_p<3K!Opp`m)vA2ElXQp$0dWc| zSdWF?UBWDqw{>v^7py81vv6@@PNoXj^^!ig2uzWP@mL76_>lg;?i3G~3asrn4r(Wr zTENi18jefQI;x(ZV@k3QL@+aIvOlJcbv?7~F)aV7JrKhTs?2tL?n?Kbbr*&k)+>RodPTq%9AMB29y&ybx*@l&6Wy zkSPh2PaOC3p&SechViRYPmb6@A%&9Eq;irq^1QEA3?`%Y!-;y2Wmj*~4L@8HuEwIN z%jzOI0mr3&pu`QO=sHMSSzVrNblxb1i4UgeSSUieL6krfSQWWuDdGJOpQ7)IV(e9# zbeBfd#^T(Ql5uRPm}Yu!>)Jowf{cW=k}mQ2PzqM-i97%&HbF_3<4Vf=gZF!NdQvcq zhR$|t^K(lMBV;}7ve$53{$r;R8QsoB3?`H8`gTEQ@5 zg|k4@qcIhcMJpCy@{<~v22Ke;{DulzKcDYuQoX>qdFnsj!fflbCps6YdET0hSrlJN zU-L|3`b8w+*)dY^>{x%deJJQq(MgZdsvPw905^iz{c?QCG5ruG_y6oHJdCcG$E}oi z%U@SA7_39oKC&poCvZP?a#B&XgOs){po?hA%R-ukaZs0OB`%aQ1`&w-{6tcx78fy{ zY(Xrh?#ed1CjAb+o@UtmLw%qG9^?`)eP>hB2Sdez*+`_2U(#2R5PK_;OD%B$q)BTG zn79b}%mk}W7a>syy+XrplUC?zqSicZ(ya@00oL|d@$;0LnmX;Qw&}p{s}%Q&;hZrJ zuN1`LK`#-H=4Ncq(4;bLw+$*RltKHqm?LH3jgon3<%E&n@z-ow9ICT4s;_7Q*3TX0 zC%RhH6d|Ih*vm~^2r63iYKvFBALDvUAXH)ZZzp)VpaZQ2rnWkLKJ1o+m|d-_ScZ#J zaF;{1AE#T{=fTeH9q6ASQO%L`E3LlpFMs?P52rGC=nc04fg^la*?oJ-@`heb^mxOZ zi@Bb|^IRktr^gNbC5b1X`qc{__y=mg$}@S<1VOWfRPe95^s2X)6U?{<&Rp7^8}nhc zn+fvOkB6ZBXUqdz&cm2>KT%oechX}F*FwZl;(;DB-~8tu8*bG7xkNsP@2Dyhc34Iq zg8M}-dBsp_#+7(kgu*{5M7!|rj9hhcKMt`=ipH8SU^WqRno>NTxz*;hx^#rhoIr+B zzkiyxwJEx^bQi;3Q9*AzWEeW3=wQ>r8HsHN%twMgQz_CeFPgb0Nm))9GCtd+0z3W= zWyQf_gY2b;JU#%JNV2;dN|k!-tDa z$VY=YZLb@s*~H$eCrQ`i>~o3oI9|)pxokr{9q?5qZ^jWve{|EtNMYRNj^l$aM#Pbc zO)e4LCW6L@SThht8-ZF{q@ZJg@W+_*wym4l#$i8cy@&;3wC%XD_}X1FSuBL zsBYG%Wh+^KglFT^jMvg{-iOQM=|#Rc^`kmJpPTwu&?Bz*dp|7m@4sjp-MM~B{Xc6n zO2!(s%R?wRaDp(m=#_q7)4{rr%Fmi9>)>Frha`ult>jiUHC4rX0P5Q>M)*rT7EPm> zv~=IUt|xHOnz8BMaHXqneT)i zj%&yupoPTq>w9@3mJF65e}+RIqlboM*F*6XmeZ$j1GWt;PVZU{0rziKlR30bEp1dgjUCQ&`q@A8@mQ& z`x;#MVxR5mq?R#WUBQvQhoaa`J(=#(9_L82t8;~xLis_Vq_JQ#jg*kA4fm1OJ+$~_ z>c+sP)@d7IytnX)gSbLMGUh}a){4aayCtO_uyL>s@>pYvqnTV?jaOFS8H#`A+qbGG z8e!>1CT#oL;;1`<18?k6aHM4m)C(j+0D(t^p9LD+b4C8qwO=@-cbJXLtuL4fWVL|< za+vM=-NIahGon?;kkPkYdr@=7!%fWOwApaLi39HyU8)IJW4k%kB2deG!A%ct|9!^9 z?=>(GlL|bV8n003At8;LC^aC0DDbInF;)jngJH;fc_lD&0-DHcRaI#< zr;(=Ej9z*3F$Yopc6n;#<5itq`bi=-<6nR#BAH@KUaFJM6#QsX-n5m(q799mjYKVoZ8x5^m<}*Q=$~Xlb1;8xtf;D=jUeh9Tm$)5vQ$;`g_R(6+%BD&C50WiL1M+`|3Y5R&Aj2 zZpMN1m{p)Y9o=0P*V!{Bwt#+XQ2*EDh=-2|&Mpy-=oK@rRqltKFmJZmNDN1tM-~*2 zii{p8H01CN$cs7BsABF%e^{@AD(Mv-9EjSdWf&kdsnb=`}(I}0n z<2|?!xEZwPU9H}ozKf&>jK=?tqCl4=0IY{JDcFBC+HgD|)+02Lofj5aaJ1abDD4HF ztQR}K5Y9G5`5w#Omg)0Jv&f-iXZs8lM*eqOK)^B$u~>Bq}%hGm~sK=Nk;p*vLGanuq=-ShKH|%dK;B$ zP!|E>5vQ)M-lf2?^A z&?JTJ*X4mo@5)JkW9lZ>@0v!BHwGvCL^Cq97$7TS)G6b$7{aS(SA}BbCir57O3`bPpVoRzEG)tX*joe1hQe5Y*^E8a6p@{#;6BErH1%g# zR$BX!tzwx#nd%_J@1Tn|?Tj;MD8d;(+n zc1J8rI@3!YrI`2Y&#M%Z!eNnJ{lN73yxtFxkm0z(H3HhHZOxDu2f% zxv!Y$E;V~nPe^eH0?j#W?9U|??NOeuHg=waVUP;>KF;@(tQ#a4(58=B_%_~G{=LgV=5$)bf00SNH{-Kqf0Ec^m4uY3gNnEfTz{wBK9{>)_j5Qcb?pJLpG#-u zqafom44XKzrOsgzq8TL9x%Suf7n;UMem{m5MkA1LQFu)jI5n4vmo_Uw%gRfcvdvuM zp$=Io@wJ;ZbxIT(zcyXXA178PAz2aW|w;i`k(l%b7 zODof5aCTSKMH_2fh?<$EB!6MXC9>(o-25JIu8ZKS&T|R)A-$zd6?igAFla$+ATFP* z1+t>$*oUvb=6aZkk15gmXk&x+yz-Kg|CeLczJ(BYl8TO5c=~nt(YTqZ!w0ou{uc@6JihTBiqGue_))Z3qh@{Mwj#!U94ct<0 z7`5wOQ-6(Kc23)ekEb^y2(=x_mSjs?zF^PGrAON&$r%6+>`hQh`JU#$6>Z*K&Axk{ z4fv~_5de#_No7GALwkQi5_nt7)ItamvuV=Qm@Zb{KmR(%nu(wpkm1e?2-q(?M6`eW_6Jgzb|j4+~ARD;W60nRWiDqt++bC?i>v(US~WEeyK04Rx3pi;ghgHaN?#Io-%KL z$*Dfdd~1*I^}(X!Qls->Q*M%^{1%u^S(iyh& zLU+ydr1VR2l9Ov0I_;VKdE$~)bl^Gi7h`y0tUOAXdoWw&Wx+<&9)6Zxn|7c?^ATa=_1KZY7TE zFq`G?-l%Ns^_+!-knp(SM9E%id7)|7Y%TG@FV)Gksp@uT-8V)3Vf#MqTpD_UIn;XS zXc>%hVs5`RU1(I!6hIhEuRvB&w11kH)gG42RK?r7=0EOikxDra8i<^pz#Q0d2_qCA z40B$lqG=jCKVNFCV-BW}`eapOP(l_0jw3PnoO5>9oe?4~JDmh-&KUED%Sz+wt!P(y z2WB&}LGn~jQB&RciD}X}grm4`j(8>f(J>eid{OV{TJAxZu5*wy zp)<6us0Mk8Y#D)uMBNXza7n}i2qyXA-^RZlZ$|>H9m^n=Cb<5{a9_FZ)rX$7J?eR? zG8<@e2wg-v8}6*}&^`TTbvs#(Io%O@ICOQ26^{#uV6~Z6t4++6h)>KreM@>P*IT?n z3~XL`DZF>*y&9(&t9IRA#4>D2tYsvBls; z)3L=*9xq0J802W;NTM%DG1`XVC@H3&Bn^-*B-0SnZ3mf{cJY2fO&$Tkgp2_ZwKTzH zOekL?LfHaPXW=toYPMKM0W$aR!3eLxs$MQEVPE-8FvjV%f3RQn0F%R(J0mFv@Dt1(f3Va+PpV~YU0WT+d+y%06m46@lE?lPIY~vR&WL;%AG}G}zm5u98 zFbeS-;$I3T=sg>@xWAvo9VKnmQAoVx-%3)aZR;n`un#}%u(_armWrmwu$G%~+a5~O z$N=;hO{L%NmJGDBI+}byamI{ynbnht|78-*kgj5ww2Fuv8`O@GmR^9wG(L_GHAAosHP_kvj%-#EoW~Zl%aHt&@jGw5 z2L>f?(X%tKI+;Mf)c!zucjb@l?#Ck|x=@Y05WE%B;C^pQZYgtoX^iGhfx(iethl(D zI2od%dY6*E-+bT9(I|3FAg$lOf>vNu8X9@Wg^Cus(>#Q>Sr51S!B$mA2Ef-D*7LMmkuoRtB!-ubbt3nW}_5I?5 zTbN~FC~L%Qv4=w;cmyj~U4r}P=Alyvx>zu{VWA&VWGV!&rV=fIqZ4oxeNi*XHPlc# zf9|=?ws~M`Le}IcB;XkY8S8YN^sBL7SYMkEySA$tP-CJVLDIhdD6~nqppX3KOc4ft zo{iB?rIyz0CsD%+tJ5lI#uOM6`*B65z|ZPH7?)@l(K(P`4~G&z#q9-E`O~19YIUde z_4yu9Qk%gscMjJROLrE$Dx1+sMEYFa@G4hQHL#-XdI69xwMQVa7RVSO8pCz$d$!wL z+A+j#ikw;Uz}AWK`DQdHb_bm#?(}(p6hPGpx|)sXNBATk;75myiY$h3t)<< zFE?T9A(l~nHiycm%ZQA4(}h}VY>FJ$4L}eyfkNML0rNJl5Zkl_G`U`<+au~K9+QUZ zFcD^;WC)K6Tnqup!uxV}9NKnrHq_zAm@Qd`7{4(CT~yLrT$!Ni=r1Z97>b0#iH%xN zk!fLm!xZnFb#Q9a?lO+A@My8*98w$3hYfA;oSK>~j0pD-EGl9Hf0wTl!2q|mUIL&?sQbD?^phSywh*AzH2FNJIq zu;qeSJ%A`%-25s25V$Yc3p1LK2YhpQNKPIK#9b}R38L%1aLy3ZN-WEWz~WGg{sC#A zP1ZeemH{!`pC8NY^BOAdPvnO(k(pV!ek~std!~WGR12%0V{S1Ocl&Vx(?cH|pVRRp zx*Gq~FJ7OlkYr3jUyv|i)Bo?XAbSu0e5w2#(rlbUL>@z)FRQ3D(+irA?tjAhth5w1 zr&69U)?1!*ze%XA-QqWqEM>n(SGkSH@JVB%CSaMujkph~JD)Wx$6)Ug2i?OYC2B{mf`VJ?%dYK))W6w}6f}=hPZ-Jk{ zd$D;Gev_Xel1R8=!lM6Tvh~k;c}eSW&`7d)3GC+Lq$TFbe*`8h7WD>9GE=|m^F^H# z;8pg}Y*=~=XO>=6^8J{hF+wk1ed#EXg4Z+wR*pB6>m7d=J6<;?sxV#*f4%izlKV6a z(AxtjO(_EgV}!O-%}T?`umW|1^6jcoZaxox&8N!08y3s%O_156xhtD6NO?3kkXz28 z8&=qsmD@i30^PQ;_nOuF+?>(&3&OvP`nCp_u1@b;KMP_v5FB7MzL>Y9yBl`znjI~V z6?I)ZypQPaM@80ZgQ|e-9_xX4Hdm&2?y0JpUBy-%k1zYoxb&I-xbdX~X>+>dB8`z% z=T{Qa&yr!Tf4xUG>_yeW`ul@RhS2>kMRBFmM&VU6cmNQM?%*} z6rtJ_ZPXL${)^VgF5=AhO6O||JDq{odUC%~IZJ3x)PeWf_RFgS_aunwVZvS~@kh5o zaV&4emcDt_bW?4G1$2CsldA2gdc}CN=$u^TquxiWhoc4>Kd|&$Fh#cPfP=2zOz=>f zGEA;9zB)PU!};%0-I};q7{rd}BU;2FmJkK8wG9Z@wOv8tq7vB>!M>0(KaTG!z^H{^ z4vL(Z;M$0mUW$3Vlf2*Lyb5oeKAB1_o~MaU@2EBKebE1gB!sqPx9P8FUzVkDHFMZ= z%(K&1go@9$*_Yw)0W^iT-=E7P&SC0*LHX~E@fGSW=boFf+an{ADL#xT*J3*mukl&w`UtgQ4&gC z^V)AP|J_tM6uoqP6$Ntq1x zX$>XWis4|JVZ_I2!!T)?AeF+(@Bau8y4mcS(^#KI!zVNsO!>^M=)}L0kKrPo8f`88 zD~hU09(qw^g{X!y-Ge{F8A{W1_$XgeQgN_QC*N@V2l@Gxk=npBUQXW`qTJ0;OLg8j zl5SmS4D^NU@B0{h5Zs8Kd3aG4cBib@b4ik=B85OtaZAH z32bEe=E!^PMuNV%n7a4hM`Xx?@gq?(n-_XT)vwX(Nv{dXDQ1e6UKJt; zeDqkii&js?wW)CQ9O4H3pyZ~!zwevxp_P8;nVqvS*KDRob;- zx!7P_dgTHn3;FiymYG}yi^1G0x2odNhssYXRWFkW|JB>bD>) z-(q(xXQMC1On>9??!uY@le-Q8I2rV(Ck!$8qF(`jX9M!HkxW;CnY|O2+$%CvMkTJ1 z^k@U#RWEzWC;B0`Kt*>eKV=IU@@_o$?Dh0eXZ~#MdB3vBCL<44AHldwz{|T zPeU$f4WTWnYc-uq1W(pslob+J7FsD#daRATU=3gMACrPT9Ea=39at&FI)wc0X3h zyjm{H7NT{xD&+*`w*|WNU4zKcAjX zzE)tl`{h3laAAq#O1ov2A6LVtYcv3B$TSrcAL2=3OY|uJ1-Bu=1$3vFk=P^Wk(0pA zV@i{Y`I~T^A4oL*^(Lq?Y3cD1Uq$)Ol?NHgt$Da^t#xCUaJZ(-ofB{0S4pZNQEO_m z*VNP3TIn{wsx|WWBeFf0Axl#ehUg4LKdJ4i^2z*!za@n)Yh<$pY$P$P!ntX1_VoW6 zvdAB4&27?@2mB}6Sn3_@*WmxHrO0aH+b>?O(v)`}Sp61iS&ftC%U;D z>&>hWkttsnup2b6W_#&MS+(&Y-#}Axm^wCFf&$TXd+07D0Q|bRbxz)( z`S&&Ukcd)+Ez&&Q1H#KIE*3~x<8xJJ(j06qoBcqJFaq)c%{RA+PV8TVu0zys91q{? zm+Wm?>(FbRjDHV!eu7dby+HQ@wrso~g1PaQQZWR={*Rx9pKhbWqYWFKOq%n3@vc5R zKVKir!3V;_Ll{3FHQ`H_BS1bQ#`Sx)=s<*^LtGE*9S~OEEEB)CkVvBCwDslgOLaZY zIDvp5gNA?*fro^EKo^66c=@aU-+t@=(tq>+{^$Q+_{{%5{b7M5g}O;sL&x*-;9Wq- MN-9Z|ix~(1KZItKMF0Q* From 60de5bbc32b6ff283d386d00ab7d07fe1564931d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 31 Dec 2020 19:07:31 +0200 Subject: [PATCH 104/680] Bump typed-ast to fix for s390x (#1892) * Bump typed-ast to fix for s390x * pipenv install typed-ast==1.4.2 --- Pipfile | 2 +- Pipfile.lock | 253 +++++++++++++++++++++++++++------------------------ setup.py | 2 +- 3 files changed, 136 insertions(+), 121 deletions(-) diff --git a/Pipfile b/Pipfile index ba596b3d738..9a4d5bd7c1b 100644 --- a/Pipfile +++ b/Pipfile @@ -28,7 +28,7 @@ mypy_extensions = ">=0.4.3" pathspec = ">=0.6" regex = ">=2020.1.8" toml = ">=0.10.1" -typed-ast = "==1.4.1" +typed-ast = "==1.4.2" typing_extensions = ">=3.7.4" black = {editable = true,extras = ["d"],path = "."} dataclasses = {"python_version <" = "3.7","version >" = "0.6"} diff --git a/Pipfile.lock b/Pipfile.lock index a5c38aa0777..dd78a3c3178 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "21836c0a63b6e3e1eacd0adec7dea61d2d5989e38225edd976ff144e499f0426" + "sha256": "3c4e23d0b6e49bac5ff2347dcb07bb4dd084d39b78c93a32359842dda401e7bf" }, "pipfile-spec": 6, "requires": {}, @@ -259,39 +259,39 @@ }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", - "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", - "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + "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" ], "index": "pypi", - "version": "==1.4.1" + "version": "==1.4.2" }, "typing-extensions": { "hashes": [ @@ -499,43 +499,58 @@ }, "coverage": { "hashes": [ - "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", - "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", - "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", - "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", - "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", - "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", - "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", - "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", - "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", - "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", - "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", - "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", - "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", - "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", - "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", - "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", - "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", - "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", - "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", - "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", - "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", - "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", - "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", - "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", - "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", - "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", - "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", - "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", - "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", - "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", - "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", - "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", - "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", - "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", + "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", + "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", + "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", + "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", + "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", + "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", + "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", + "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", + "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", + "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", + "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", + "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", + "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", + "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", + "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", + "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", + "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", + "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", + "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", + "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", + "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", + "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", + "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", + "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", + "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", + "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", + "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", + "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", + "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", + "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", + "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", + "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", + "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", + "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", + "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", + "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", + "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", + "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", + "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", + "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", + "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", + "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", + "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", + "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", + "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", + "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", + "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", + "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" ], "index": "pypi", - "version": "==5.3" + "version": "==5.3.1" }, "distlib": { "hashes": [ @@ -610,11 +625,11 @@ }, "keyring": { "hashes": [ - "sha256:12de23258a95f3b13e5b167f7a641a878e91eab8ef16fafc077720a95e6115bb", - "sha256:207bd66f2a9881c835dad653da04e196c678bf104f8252141d2d3c4f31051579" + "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675", + "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea" ], "markers": "python_version >= '3.6'", - "version": "==21.5.0" + "version": "==21.8.0" }, "markupsafe": { "hashes": [ @@ -805,10 +820,10 @@ }, "pytz": { "hashes": [ - "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", - "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" + "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", + "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" ], - "version": "==2020.4" + "version": "==2020.5" }, "pyyaml": { "hashes": [ @@ -838,11 +853,11 @@ }, "recommonmark": { "hashes": [ - "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", - "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852" + "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f", + "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.7.1" }, "regex": { "hashes": [ @@ -893,11 +908,11 @@ }, "requests": { "hashes": [ - "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", - "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.25.0" + "version": "==2.25.1" }, "requests-toolbelt": { "hashes": [ @@ -944,11 +959,11 @@ }, "sphinx": { "hashes": [ - "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300", - "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960" + "sha256:aeef652b14629431c82d3fe994ce39ead65b3fe87cf41b9a3714168ff8b83376", + "sha256:e450cb205ff8924611085183bf1353da26802ae73d9251a8fcdf220a8f8712ef" ], "index": "pypi", - "version": "==3.3.1" + "version": "==3.4.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1008,55 +1023,55 @@ }, "tqdm": { "hashes": [ - "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5", - "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1" + "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416", + "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.54.1" + "version": "==4.55.0" }, "twine": { "hashes": [ - "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", - "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" + "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", + "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.3.0" }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", - "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", - "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + "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" ], "index": "pypi", - "version": "==1.4.1" + "version": "==1.4.2" }, "typing-extensions": { "hashes": [ diff --git a/setup.py b/setup.py index 14bc1ef586a..c97dd35fe28 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,7 @@ def get_long_description() -> str: "click>=7.1.2", "appdirs", "toml>=0.10.1", - "typed-ast>=1.4.0", + "typed-ast>=1.4.2", "regex>=2020.1.8", "pathspec>=0.6, <1", "dataclasses>=0.6; python_version < '3.7'", From af6f78f2ae408afebb375043930bbdd4c26599f5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 31 Dec 2020 09:09:07 -0800 Subject: [PATCH 105/680] Add pyi file support to .pre-commit-hooks.yaml (#1875) Since pre-commit 2.9.0 (2020-11-21), the types_or key can be used to match multiple disparate file types. For more upstream details, see: https://github.com/pre-commit/pre-commit/issues/607 Add the minimum_pre_commit_version to require pre-commit 2.9.0+. Fixes #402 --- .pre-commit-config.yaml | 3 ++- .pre-commit-hooks.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e12e46f8d8..c2580d2d601 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,8 +8,9 @@ repos: name: black language: system entry: black + minimum_pre_commit_version: 2.9.0 require_serial: true - types: [python] + types_or: [python, pyi] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.1 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index c59213e2632..c575842e2bf 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,5 +4,6 @@ entry: black language: python language_version: python3 + minimum_pre_commit_version: 2.9.0 require_serial: true - types: [python] + types_or: [python, pyi] From 33d861371533d64a54af419798f1ddfa692f388c Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 31 Dec 2020 11:10:18 -0800 Subject: [PATCH 106/680] Bump minimum_pre_commit_version per recommendation (#1895) Recommended by @asottile, the pre-commit author and maintainer, to avoid some breakages in version 2.9.0. --- .pre-commit-config.yaml | 2 +- .pre-commit-hooks.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2580d2d601..dc95e902053 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: name: black language: system entry: black - minimum_pre_commit_version: 2.9.0 + minimum_pre_commit_version: 2.9.2 require_serial: true types_or: [python, pyi] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index c575842e2bf..de2eb674e0d 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,6 +4,6 @@ entry: black language: python language_version: python3 - minimum_pre_commit_version: 2.9.0 + minimum_pre_commit_version: 2.9.2 require_serial: true types_or: [python, pyi] From f07832eac44eb38f8b048aae3e1de3512a06057c Mon Sep 17 00:00:00 2001 From: "Paul \"TBBle\" Hampson" Date: Sat, 2 Jan 2021 05:54:35 +1100 Subject: [PATCH 107/680] Use Prettier pre-commit mirror (#1896) This fixes Prettier install failures similar to those seen in https://github.com/prettier/prettier/issues/9459, and is the solution recommended there. Signed-off-by: Paul "TBBle" Hampson --- .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 dc95e902053..9955a24baf8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,8 +24,8 @@ repos: - id: mypy exclude: ^docs/conf.py - - repo: https://github.com/prettier/prettier - rev: 1.19.1 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v1.19.1 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] From 966baaacbc24a4affa88ea5bb0b5a44b0f52fe2f Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen Date: Sun, 3 Jan 2021 17:14:59 +0100 Subject: [PATCH 108/680] Only require typing-extensions if Python < 3.8 (#1873) --- Pipfile | 2 +- Pipfile.lock | 95 +++++++++++++++++++++++++++++++++++-- setup.py | 2 +- src/black/__init__.py | 6 ++- src/blib2to3/pgen2/token.py | 7 ++- 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/Pipfile b/Pipfile index 9a4d5bd7c1b..32fb867415f 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,6 @@ pathspec = ">=0.6" regex = ">=2020.1.8" toml = ">=0.10.1" typed-ast = "==1.4.2" -typing_extensions = ">=3.7.4" +typing_extensions = {"python_version <" = "3.8","version >=" = "3.7.4"} black = {editable = true,extras = ["d"],path = "."} dataclasses = {"python_version <" = "3.7","version >" = "0.6"} diff --git a/Pipfile.lock b/Pipfile.lock index dd78a3c3178..91fd3a49556 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3c4e23d0b6e49bac5ff2347dcb07bb4dd084d39b78c93a32359842dda401e7bf" + "sha256": "1b075e0e344dad54944ed4474351c3f311accb67cbea2e0c381da3700b71f415" }, "pipfile-spec": 6, "requires": {}, @@ -300,7 +300,9 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.3" + "python_version <": "3.8", + "version": "==3.7.4.3", + "version >=": "3.7.4" }, "yarl": { "hashes": [ @@ -459,6 +461,47 @@ ], "version": "==2020.12.5" }, + "cffi": { + "hashes": [ + "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", + "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", + "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", + "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", + "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", + "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", + "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", + "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", + "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", + "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", + "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", + "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", + "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", + "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", + "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", + "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", + "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", + "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", + "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", + "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", + "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", + "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", + "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", + "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", + "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", + "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", + "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", + "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", + "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", + "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", + "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", + "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", + "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", + "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", + "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", + "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" + ], + "version": "==1.14.4" + }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -552,6 +595,26 @@ "index": "pypi", "version": "==5.3.1" }, + "cryptography": { + "hashes": [ + "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", + "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7", + "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901", + "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c", + "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244", + "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6", + "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5", + "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e", + "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c", + "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0", + "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812", + "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a", + "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", + "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==3.3.1" + }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -615,6 +678,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, + "jeepney": { + "hashes": [ + "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", + "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.6.0" + }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", @@ -794,6 +865,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -928,6 +1007,14 @@ ], "version": "==1.4.0" }, + "secretstorage": { + "hashes": [ + "sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa", + "sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.0" + }, "setuptools-scm": { "hashes": [ "sha256:1fc4e25df445351d172bb4788f4d07f9e9ce0e8b7dee6b19584e46110172ca13", @@ -1080,7 +1167,9 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.3" + "python_version <": "3.8", + "version": "==3.7.4.3", + "version >=": "3.7.4" }, "urllib3": { "hashes": [ diff --git a/setup.py b/setup.py index c97dd35fe28..48160568265 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def get_long_description() -> str: "regex>=2020.1.8", "pathspec>=0.6, <1", "dataclasses>=0.6; python_version < '3.7'", - "typing_extensions>=3.7.4", + "typing_extensions>=3.7.4; python_version < '3.8'", "mypy_extensions>=0.4.3", ], extras_require={ diff --git a/src/black/__init__.py b/src/black/__init__.py index c7c5d724a9f..9b9903c6645 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -42,7 +42,6 @@ cast, TYPE_CHECKING, ) -from typing_extensions import Final from mypy_extensions import mypyc_attr from appdirs import user_cache_dir @@ -61,6 +60,11 @@ from _black_version import version as __version__ +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + if TYPE_CHECKING: import colorama # noqa: F401 diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py index 5870d47a61a..1e0dec9c714 100644 --- a/src/blib2to3/pgen2/token.py +++ b/src/blib2to3/pgen2/token.py @@ -1,7 +1,12 @@ """Token constants (from "token.h").""" +import sys from typing import Dict -from typing_extensions import Final + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final # Taken from Python (r53757) and modified to include some tokens # originally monkeypatched in by pgen2.tokenize From ba3648d98471a6d0ad951d7f75ac512108173bc7 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 8 Jan 2021 20:47:03 +0100 Subject: [PATCH 109/680] Release gh action (#1909) * Gets gh-action ready for marketplace release * Updates documentation and removes redundant gh-action input argument * Fixes gh-action bug This commit fixes a bug which caused not all input arguments were forwarder to the black formatter. * Update README.md Co-authored-by: Cooper Lees Co-authored-by: Cooper Lees --- README.md | 8 ++++++++ action.yml | 5 +++++ action/entrypoint.sh | 17 ++++++++++++----- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a557a6fc6c8..269bf5aaa43 100644 --- a/README.md +++ b/README.md @@ -411,8 +411,16 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@stable + with: + black_args: ". --check" ``` +### Inputs + +#### `black_args` + +**optional**: Black input arguments. Defaults to `. --check --diff`. + ## Ignoring unmodified files _Black_ remembers files it has already formatted, unless the `--diff` flag is used or diff --git a/action.yml b/action.yml index 60bf369a3d8..59b16a9fb6c 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,11 @@ name: "Black" description: "The uncompromising Python code formatter." author: "Łukasz Langa and contributors to Black" +inputs: + black_args: + description: "Black input arguments." + required: false + default: "" branding: color: "black" icon: "check-circle" diff --git a/action/entrypoint.sh b/action/entrypoint.sh index dc86fa1996e..92501f79db8 100755 --- a/action/entrypoint.sh +++ b/action/entrypoint.sh @@ -1,10 +1,17 @@ #!/bin/sh set -e -if [ $# -eq 0 ]; then - # Default (if no args provided). - sh -c "black . --check --diff" +# If no arguments are given use current working directory +black_args=(".") +if [ "$#" -eq 0 ] && [ "${INPUT_BLACK_ARGS}" != "" ]; then + black_args+=(${INPUT_BLACK_ARGS}) +elif [ "$#" -ne 0 ] && [ "${INPUT_BLACK_ARGS}" != "" ]; then + black_args+=($* ${INPUT_BLACK_ARGS}) +elif [ "$#" -ne 0 ] && [ "${INPUT_BLACK_ARGS}" == "" ]; then + black_args+=($*) else - # Custom args. - sh -c "black $*" + # Default (if no args provided). + black_args+=("--check" "--diff") fi + +sh -c "black . ${black_args[*]}" From a81702ce027ef6792955d5db02aad60a34c7ec44 Mon Sep 17 00:00:00 2001 From: Troy Murray Date: Sat, 9 Jan 2021 07:26:42 +1100 Subject: [PATCH 110/680] Changed max workers on windows to 60 (#1912) --- 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 9b9903c6645..91f70d96165 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -741,7 +741,7 @@ def reformat_many( worker_count = os.cpu_count() if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 - worker_count = min(worker_count, 61) + worker_count = min(worker_count, 60) try: executor = ProcessPoolExecutor(max_workers=worker_count) except (ImportError, OSError): From ce269d2da52596f549e54c94726b77fa1adad177 Mon Sep 17 00:00:00 2001 From: Shota Ray Imaki Date: Tue, 12 Jan 2021 01:21:14 +0900 Subject: [PATCH 111/680] fix #1917 (#1918) --- docs/compatible_configs/isort/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatible_configs/isort/pyproject.toml b/docs/compatible_configs/isort/pyproject.toml index cea9915bebf..2dc02c8c50c 100644 --- a/docs/compatible_configs/isort/pyproject.toml +++ b/docs/compatible_configs/isort/pyproject.toml @@ -1,2 +1,2 @@ [tool.isort] -profile = black +profile = 'black' From 3d0689470752daedf7ef2bc9e602478ea31c0430 Mon Sep 17 00:00:00 2001 From: Thomas Hagebols Date: Mon, 11 Jan 2021 17:23:11 +0100 Subject: [PATCH 112/680] Set gh action entrypoint interpreter to bash (#1919) --- action/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action/entrypoint.sh b/action/entrypoint.sh index 92501f79db8..50f9472cc5f 100755 --- a/action/entrypoint.sh +++ b/action/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e # If no arguments are given use current working directory From 4310f39bd94c7fa29fc31079247d2aab4bd50f1a Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 13 Jan 2021 15:17:30 -0800 Subject: [PATCH 113/680] Update Contributing Docs (#1915) * Update Contributing Docs - Update docs with all new tox hotness - Test running docs build: - `sphinx-build -a -b html -W docs/ docs/_build/` Fixes #1907 * Fix docs/contributing_to_black.md lint * Remove autogenerated copy pasta * Fix review typos + regen automated docs via Running Sphinx v1.8.5 --- CONTRIBUTING.md | 32 ++++++++++++++++++++++++++--- docs/change_log.md | 11 ++++++++-- docs/contributing_to_black.md | 32 ++++++++++++++++++++++++++--- docs/github_actions.md | 12 ++++++++--- docs/installation_and_usage.md | 7 ++++++- docs/version_control_integration.md | 5 ----- test_requirements.txt | 4 +++- 7 files changed, 85 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4250f78718..571d870452b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,14 +34,40 @@ If you haven't used `pipenv` before but are comfortable with virtualenvs, just r `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. -Before submitting pull requests, run lints and tests with: +Non pipenv install works too: ```console +$ pip install -r test_requirements +$ pip install -e .[d] +``` + +Before submitting pull requests, run lints and tests with the following commands from +the root of the black repo: + +```console +# Linting $ pre-commit run -a -$ python -m unittest + +# Unit tests +$ tox -e py + +# Optional Fuzz testing +$ tox -e fuzz + +# Optional CI run to test your changes on many popular python projects $ black-primer [-k -w /tmp/black_test_repos] ``` +### Docs Testing + +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/ +``` + ## black-primer `black-primer` is used by CI to pull down well-known _Black_ formatted projects and see @@ -52,7 +78,7 @@ your PR. You may need to change configuration for it to pass. For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md#black-primer). +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). ## Hygiene diff --git a/docs/change_log.md b/docs/change_log.md index e6afefbf686..066be76c06c 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -23,8 +23,15 @@ - Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) -- Added `--stdin-filename` argument to allow stdin to respect `--force-exclude` rules. - Works very alike to flake8's `--stdin-display-name` (#1780) +- Added parsing support for unparenthesized tuples and yield expressions in annotated + assignments (#1835) + +- use lowercase hex strings (#1692) + +#### _Packaging_ + +- Self-contained native _Black_ binaries are now provided for releases via GitHub + Releases (#1743) ### 20.8b1 diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md index f0be872a578..562b43a76ac 100644 --- a/docs/contributing_to_black.md +++ b/docs/contributing_to_black.md @@ -36,14 +36,40 @@ If you haven't used `pipenv` before but are comfortable with virtualenvs, just r `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. -Before submitting pull requests, run lints and tests with: +Non pipenv install works too: ```console +$ pip install -r test_requirements +$ pip install -e .[d] +``` + +Before submitting pull requests, run lints and tests with the following commands from +the root of the black repo: + +```console +# Linting $ pre-commit run -a -$ python -m unittest + +# Unit tests +$ tox -e py + +# Optional Fuzz testing +$ tox -e fuzz + +# Optional CI run to test your changes on many popular python projects $ black-primer [-k -w /tmp/black_test_repos] ``` +### Docs Testing + +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/ +``` + ## black-primer `black-primer` is used by CI to pull down well-known _Black_ formatted projects and see @@ -54,7 +80,7 @@ your PR. You may need to change configuration for it to pass. For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md#). +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). ## Hygiene diff --git a/docs/github_actions.md b/docs/github_actions.md index ac80c2fab6f..bd4680998d9 100644 --- a/docs/github_actions.md +++ b/docs/github_actions.md @@ -15,7 +15,13 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@stable # the default is equivalent to `black . --diff --check`. - with: # (optional - override the default parameters). - args: ". --diff --check" + - uses: psf/black@stable + with: + black_args: ". --check" ``` + +## Inputs + +### `black_args` + +**optional**: Black input arguments. Defaults to `. --check --diff`. diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index c91a1c400ef..d0dd0c99dc5 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -90,6 +90,11 @@ Options: matching this regex will be excluded even when they are passed explicitly as arguments. + --stdin-filename TEXT 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. + -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with 2>/dev/null. @@ -119,7 +124,7 @@ about _Black_'s changes or will overwrite _Black_'s changes. A good example of t should be configured to neither warn about nor overwrite _Black_'s changes. Actual details on _Black_ compatible configurations for various tools can be found in -[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#). +[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#black-compatible-configurations). ## Migrating your code style without ruining git blame diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md index 0e09854a2e5..2d8bc172eba 100644 --- a/docs/version_control_integration.md +++ b/docs/version_control_integration.md @@ -23,11 +23,6 @@ for your project. See _Black_'s own [pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an example. -When using the `--diff` flag with `pre-commit`, you must also use the `--check` flag. -When you want to run _Black_ only on specific files in pre-commit, either use -pre-commit's own `files` and `exclude` or, when using _Black_'s `--include`, set -`--force-exclude` to the negated regex of `--include`. - If you're already using Python 3.7, switch the `language_version` accordingly. Finally, `stable` is a branch that tracks the latest release on PyPI. If you'd rather run on master, this is also an option. diff --git a/test_requirements.txt b/test_requirements.txt index 9f69b8edf83..a1464e90686 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,5 +1,7 @@ +coverage >= 5.3 +pre-commit pytest >= 6.1.1 pytest-mock >= 3.3.1 pytest-cases >= 2.3.0 -coverage >= 5.3 parameterized >= 0.7.4 +tox From b55fb821e795bc45903e6f7b89e8857ddb5a94f8 Mon Sep 17 00:00:00 2001 From: Peter Stensmyr Date: Fri, 15 Jan 2021 10:57:45 +1100 Subject: [PATCH 114/680] Update link pointing to how-black-wraps-lines (#1925) The section about the Black code style has been moved to its own file. Update link on the compatible configs page to point to the right place. --- docs/compatible_configs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index c6724e77e57..a4f83ee472f 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -221,7 +221,7 @@ max-line-length = 88 ### Why those options above? When _Black_ is folding very long expressions, the closing brackets will -[be dedented](https://github.com/psf/black#how-black-wraps-lines). +[be dedented](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#how-black-wraps-lines). ```py3 ImportantClass.important_method( From e8aadedd970140a840e8120bd36e8df52cc26ed2 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Fri, 15 Jan 2021 23:43:23 +0100 Subject: [PATCH 115/680] Fix typo (#1931) --- src/black_primer/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 5c5576e1ff3..c3069812b22 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -21,7 +21,7 @@ WINDOWS = system() == "Windows" BLACK_BINARY = "black.exe" if WINDOWS else "black" -GIT_BIANRY = "git.exe" if WINDOWS else "git" +GIT_BINARY = "git.exe" if WINDOWS else "git" LOG = logging.getLogger(__name__) @@ -160,7 +160,7 @@ async def git_checkout_or_rebase( depth: int = 1, ) -> Optional[Path]: """git Clone project or rebase""" - git_bin = str(which(GIT_BIANRY)) + git_bin = str(which(GIT_BINARY)) if not git_bin: LOG.error("No git binary found") return None From de510478d9b12490eddf93a785494f726897363b Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sat, 16 Jan 2021 11:05:15 +1100 Subject: [PATCH 116/680] OSS-Fuzz integration (#1930) --- fuzz.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fuzz.py b/fuzz.py index 604e6ced291..6330c58cc81 100644 --- a/fuzz.py +++ b/fuzz.py @@ -56,4 +56,17 @@ def test_idempotent_any_syntatically_valid_python( if __name__ == "__main__": + # Run tests, including shrinking and reporting any known failures. test_idempotent_any_syntatically_valid_python() + + # If Atheris is available, run coverage-guided fuzzing. + # (if you want only bounded fuzzing, just use `pytest fuzz.py`) + try: + import sys + import atheris + except ImportError: + pass + else: + test = test_idempotent_any_syntatically_valid_python + atheris.Setup(sys.argv, test.hypothesis.fuzz_one_input) + atheris.Fuzz() From 692c0f50d91e3163bb87401e4a0e070b2eb5b163 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 17 Jan 2021 16:59:06 -0800 Subject: [PATCH 117/680] Add --skip-magic-trailing-comma (#1824) --- README.md | 19 +- docs/blackd.md | 3 + docs/installation_and_usage.md | 18 +- docs/the_black_code_style.md | 3 + src/black/__init__.py | 89 ++-- src/blackd/__init__.py | 6 + .../expression_skip_magic_trailing_comma.diff | 404 ++++++++++++++++++ tests/test_black.py | 25 ++ 8 files changed, 531 insertions(+), 36 deletions(-) create mode 100644 tests/data/expression_skip_magic_trailing_comma.diff diff --git a/README.md b/README.md index 269bf5aaa43..f1ec76938f0 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,10 @@ Options: -S, --skip-string-normalization Don't normalize string quotes or prefixes. + -C, --skip-magic-trailing-comma + Don't use trailing commas as a reason to + split lines. + --check Don't write the files back, just return the status. Return code 0 means nothing would change. Return code 1 means some files @@ -127,18 +131,19 @@ Options: paths are excluded. Use forward slashes for directories on all platforms (Windows, too). Exclusions are calculated first, inclusions - later. [default: /(\.eggs|\.git|\.hg|\.mypy - _cache|\.nox|\.tox|\.venv|\.svn|_build|buck- - out|build|dist)/] + later. [default: /(\.direnv|\.eggs|\.git|\. + hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu + ild|buck-out|build|dist)/] --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even - when they are passed explicitly as arguments. + when they are passed explicitly as + arguments. --stdin-filename TEXT 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. + stdin. Useful to make sure Black will + respect --force-exclude option on some + editors that rely on using stdin. -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with diff --git a/docs/blackd.md b/docs/blackd.md index c341308e1e4..c8058ee7c63 100644 --- a/docs/blackd.md +++ b/docs/blackd.md @@ -54,6 +54,9 @@ The headers controlling how source code is formatted are: - `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. +- `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 + will not be used as a reason to split lines. - `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/installation_and_usage.md b/docs/installation_and_usage.md index d0dd0c99dc5..e3b53fd076e 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -52,6 +52,10 @@ Options: -S, --skip-string-normalization Don't normalize string quotes or prefixes. + -C, --skip-magic-trailing-comma + Don't use trailing commas as a reason to + split lines. + --check Don't write the files back, just return the status. Return code 0 means nothing would change. Return code 1 means some files @@ -82,13 +86,19 @@ Options: paths are excluded. Use forward slashes for directories on all platforms (Windows, too). Exclusions are calculated first, inclusions - later. [default: /(\.eggs|\.git|\.hg|\.mypy - _cache|\.nox|\.tox|\.venv|\.svn|_build|buck- - out|build|dist)/] + later. [default: /(\.direnv|\.eggs|\.git|\. + hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu + ild|buck-out|build|dist)/] --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even - when they are passed explicitly as arguments. + when they are passed explicitly as + arguments. + + --stdin-filename TEXT 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. --stdin-filename TEXT The name of the file when passing it through stdin. Useful to make sure Black will respect diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 19464ba482a..a4e55c1744c 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -438,6 +438,9 @@ into one item per line. How do you make it stop? Just delete that trailing comma and _Black_ will collapse your collection into one line if it fits. +If you must, you can recover the behaviour of early versions of Black with the option +`--skip-magic-trailing-comma` / `-C`. + ### r"strings" and R"strings" _Black_ normalizes string quotes as well as string prefixes, making them lowercase. One diff --git a/src/black/__init__.py b/src/black/__init__.py index 91f70d96165..9034bf6cf77 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -260,6 +260,7 @@ class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) line_length: int = DEFAULT_LINE_LENGTH string_normalization: bool = True + magic_trailing_comma: bool = True experimental_string_processing: bool = False is_pyi: bool = False @@ -397,6 +398,12 @@ def target_version_option_callback( is_flag=True, help="Don't normalize string quotes or prefixes.", ) +@click.option( + "-C", + "--skip-magic-trailing-comma", + is_flag=True, + help="Don't use trailing commas as a reason to split lines.", +) @click.option( "--experimental-string-processing", is_flag=True, @@ -524,6 +531,7 @@ def main( fast: bool, pyi: bool, skip_string_normalization: bool, + skip_magic_trailing_comma: bool, experimental_string_processing: bool, quiet: bool, verbose: bool, @@ -546,6 +554,7 @@ def main( line_length=line_length, is_pyi=pyi, string_normalization=not skip_string_normalization, + magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, ) if config and verbose: @@ -1022,13 +1031,12 @@ def f( versions = detect_target_versions(src_node) normalize_fmt_off(src_node) lines = LineGenerator( + mode=mode, remove_u_prefix="unicode_literals" in future_imports or supports_feature(versions, Feature.UNICODE_LITERALS), - is_pyi=mode.is_pyi, - normalize_strings=mode.string_normalization, ) elt = EmptyLineTracker(is_pyi=mode.is_pyi) - empty_line = Line() + empty_line = Line(mode=mode) after = 0 split_line_features = { feature @@ -1464,6 +1472,7 @@ def get_open_lsqb(self) -> Optional[Leaf]: class Line: """Holds leaves and comments. Can be printed with `str(line)`.""" + mode: Mode depth: int = 0 leaves: List[Leaf] = field(default_factory=list) # keys ordered like `leaves` @@ -1496,8 +1505,11 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: ) if self.inside_brackets or not preformatted: self.bracket_tracker.mark(leaf) - if self.maybe_should_explode(leaf): - self.should_explode = True + if self.mode.magic_trailing_comma: + if self.has_magic_trailing_comma(leaf): + self.should_explode = True + elif self.has_magic_trailing_comma(leaf, ensure_removable=True): + self.remove_trailing_comma() if not self.append_comment(leaf): self.leaves.append(leaf) @@ -1673,10 +1685,14 @@ def contains_unsplittable_type_ignore(self) -> bool: def contains_multiline_strings(self) -> bool: return any(is_multiline_string(leaf) for leaf in self.leaves) - def maybe_should_explode(self, closing: Leaf) -> bool: - """Return True if this line should explode (always be split), that is when: - - there's a trailing comma here; and - - it's not a one-tuple. + def has_magic_trailing_comma( + self, closing: Leaf, ensure_removable: bool = False + ) -> bool: + """Return True if we have a magic trailing comma, that is when: + - there's a trailing comma here + - it's not a one-tuple + Additionally, if ensure_removable: + - it's not from square bracket indexing """ if not ( closing.type in CLOSING_BRACKETS @@ -1685,9 +1701,15 @@ def maybe_should_explode(self, closing: Leaf) -> bool: ): return False - if closing.type in {token.RBRACE, token.RSQB}: + if closing.type == token.RBRACE: return True + if closing.type == token.RSQB: + if not ensure_removable: + return True + comma = self.leaves[-1] + return bool(comma.parent and comma.parent.type == syms.listmaker) + if self.is_import: return True @@ -1765,6 +1787,7 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: def clone(self) -> "Line": return Line( + mode=self.mode, depth=self.depth, inside_brackets=self.inside_brackets, should_explode=self.should_explode, @@ -1923,10 +1946,9 @@ class LineGenerator(Visitor[Line]): in ways that will no longer stringify to valid Python code on the tree. """ - is_pyi: bool = False - normalize_strings: bool = True - current_line: Line = field(default_factory=Line) + mode: Mode remove_u_prefix: bool = False + current_line: Line = field(init=False) def line(self, indent: int = 0) -> Iterator[Line]: """Generate a line. @@ -1941,7 +1963,7 @@ def line(self, indent: int = 0) -> Iterator[Line]: return # Line is empty, don't emit. Creating a new one unnecessary. complete_line = self.current_line - self.current_line = Line(depth=complete_line.depth + indent) + self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) yield complete_line def visit_default(self, node: LN) -> Iterator[Line]: @@ -1965,7 +1987,7 @@ def visit_default(self, node: LN) -> Iterator[Line]: yield from self.line() normalize_prefix(node, inside_brackets=any_open_brackets) - if self.normalize_strings and node.type == token.STRING: + if self.mode.string_normalization and node.type == token.STRING: normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix) normalize_string_quotes(node) if node.type == token.NUMBER: @@ -2017,7 +2039,7 @@ def visit_stmt( def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" - if self.is_pyi and is_stub_suite(node): + if self.mode.is_pyi and is_stub_suite(node): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -2026,7 +2048,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: """Visit a statement without nested statements.""" is_suite_like = node.parent and node.parent.type in STATEMENT if is_suite_like: - if self.is_pyi and is_stub_body(node): + if self.mode.is_pyi and is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) @@ -2034,7 +2056,11 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: yield from self.line(-1) else: - if not self.is_pyi or not node.parent or not is_stub_suite(node.parent): + if ( + not self.mode.is_pyi + or not node.parent + or not is_stub_suite(node.parent) + ): yield from self.line() yield from self.visit_default(node) @@ -2110,6 +2136,8 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: def __post_init__(self) -> None: """You are in a twisty little maze of passages.""" + self.current_line = Line(mode=self.mode) + v = self.visit_stmt Ø: Set[str] = set() self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","}) @@ -4350,6 +4378,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: # `StringSplitter` will break it down further if necessary. string_value = LL[string_idx].value string_line = Line( + mode=line.mode, depth=line.depth + 1, inside_brackets=True, should_explode=line.should_explode, @@ -4943,7 +4972,7 @@ def bracket_split_build_line( 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. """ - result = Line(depth=original.depth) + result = Line(mode=original.mode, depth=original.depth) if is_body: result.inside_brackets = True result.depth += 1 @@ -5015,7 +5044,9 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[ if bt.delimiter_count_with_priority(delimiter_priority) == 1: raise CannotSplit("Splitting a single attribute from its owner looks wrong") - current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets) + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) lowest_depth = sys.maxsize trailing_comma_safe = True @@ -5027,7 +5058,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: except ValueError: yield current_line - current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets) + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) current_line.append(leaf) for leaf in line.leaves: @@ -5051,7 +5084,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: if leaf_priority == delimiter_priority: yield current_line - current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets) + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) if current_line: if ( trailing_comma_safe @@ -5072,7 +5107,9 @@ def standalone_comment_split( if not line.contains_standalone_comments(0): raise CannotSplit("Line does not have any standalone comments") - current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets) + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) def append_to_line(leaf: Leaf) -> Iterator[Line]: """Append `leaf` to current line or to new line if appending impossible.""" @@ -5082,7 +5119,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: except ValueError: yield current_line - current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets) + current_line = Line( + line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) current_line.append(leaf) for leaf in line.leaves: @@ -5767,7 +5806,7 @@ def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: return False return max_priority == COMMA_PRIORITY and ( - trailing_comma + (line.mode.magic_trailing_comma and trailing_comma) # always explode imports or opening_bracket.parent.type in {syms.atom, syms.import_from} ) diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index f77a5e8e7be..fc684730e4b 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -32,6 +32,7 @@ LINE_LENGTH_HEADER = "X-Line-Length" PYTHON_VARIANT_HEADER = "X-Python-Variant" SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" +SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" DIFF_HEADER = "X-Diff" @@ -40,6 +41,7 @@ LINE_LENGTH_HEADER, PYTHON_VARIANT_HEADER, SKIP_STRING_NORMALIZATION_HEADER, + SKIP_MAGIC_TRAILING_COMMA, FAST_OR_SAFE_HEADER, DIFF_HEADER, ] @@ -114,6 +116,9 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: 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) + ) fast = False if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast": fast = True @@ -122,6 +127,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: is_pyi=pyi, line_length=line_length, string_normalization=not skip_string_normalization, + magic_trailing_comma=not skip_magic_trailing_comma, ) req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff new file mode 100644 index 00000000000..8a0225bceb5 --- /dev/null +++ b/tests/data/expression_skip_magic_trailing_comma.diff @@ -0,0 +1,404 @@ +--- [Deterministic header] ++++ [Deterministic header] +@@ -1,8 +1,8 @@ + ... +-'some_string' +-b'\\xa3' ++"some_string" ++b"\\xa3" + Name + None + True + False + 1 +@@ -29,63 +29,84 @@ + ~great + +value + -1 + ~int and not v1 ^ 123 + v2 | True + (~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))) ++flags & ~select.EPOLLIN and waiters.write_task is not None + lambda arg: None + lambda a=True: a + lambda a, b, c=True: a +-lambda a, b, c=True, *, d=(1 << v2), e='str': a +-lambda a, b, c=True, *vararg, d=(v1 << 2), e='str', **kwargs: a + b ++lambda a, b, c=True, *, d=(1 << v2), e="str": a ++lambda a, b, c=True, *vararg, d=(v1 << 2), e="str", **kwargs: a + b + manylambdas = lambda x=lambda y=lambda z=1: z: y(): x() +-foo = (lambda port_id, ignore_missing: {"port1": port1_resource, "port2": port2_resource}[port_id]) ++foo = lambda port_id, ignore_missing: { ++ "port1": port1_resource, ++ "port2": port2_resource, ++}[port_id] + 1 if True else 2 + str or None if True else str or bytes or None + (str or None) if True else (str or bytes or None) + str or None if (1 if True else 2) else str or bytes or None + (str or None) if (1 if True else 2) else (str or bytes or None) +-((super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None)) +-{'2.7': dead, '3.7': (long_live or die_hard)} +-{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}} ++( ++ (super_long_variable_name or None) ++ if (1 if super_long_test_name else 2) ++ else (str or bytes or None) ++) ++{"2.7": dead, "3.7": (long_live or die_hard)} ++{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} + {**a, **b, **c} +-{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')} +-({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None ++{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} ++({"a": "b"}, (True or False), (+value), "string", b"bytes") or None + () + (1,) + (1, 2) + (1, 2, 3) + [] + [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] +-[1, 2, 3,] ++[1, 2, 3] + [*a] + [*range(10)] +-[*a, 4, 5,] +-[4, *a, 5,] +-[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more] ++[*a, 4, 5] ++[4, *a, 5] ++[ ++ this_is_a_very_long_variable_which_will_force_a_delimiter_split, ++ element, ++ another, ++ *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) 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) 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"))} + {a: b * 2 for a, b in dictionary.items()} + {a: b * -2 for a, b in dictionary.items()} +-{k: v for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension} ++{ ++ k: v ++ for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension ++} + Python3 > Python2 > COBOL + Life is Life + call() + call(arg) +-call(kwarg='hey') +-call(arg, kwarg='hey') +-call(arg, another, kwarg='hey', **kwargs) +-call(this_is_a_very_long_variable_which_will_force_a_delimiter_split, arg, another, kwarg='hey', **kwargs) # note: no trailing comma pre-3.6 ++call(kwarg="hey") ++call(arg, kwarg="hey") ++call(arg, another, kwarg="hey", **kwargs) ++call( ++ this_is_a_very_long_variable_which_will_force_a_delimiter_split, ++ arg, ++ another, ++ kwarg="hey", ++ **kwargs ++) # note: no trailing comma pre-3.6 + call(*gidgets[:2]) + call(a, *gidgets[:2]) + call(**self.screen_kwargs) + call(b, **self.screen_kwargs) + lukasz.langa.pl +@@ -94,26 +115,24 @@ + 1.0 .real + ....__class__ + list[str] + dict[str, int] + tuple[str, ...] +-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]]]], + ] + xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) + ) + xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) + ) +-xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[ +- ..., List[SomeClass] +-] = classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore ++xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( ++ sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) ++) # type: ignore + slice[0] + slice[0:1] + slice[0:1:2] + slice[:] + slice[:-1] +@@ -137,113 +156,178 @@ + numpy[-(c + 1) :, d] + numpy[:, l[-2]] + numpy[:, ::-1] + numpy[np.newaxis, :] + (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) +-{'2.7': dead, '3.7': long_live or die_hard} +-{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'} ++{"2.7": dead, "3.7": long_live or die_hard} ++{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] + (SomeName) + 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) 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} ++{ ++ "id": "1", ++ "type": "type", ++ "started_at": now(), ++ "ended_at": now() + timedelta(days=10), ++ "priority": 1, ++ "import_session_id": 1, ++ **kwargs, ++} + a = (1,) +-b = 1, ++b = (1,) + c = 1 + d = (1,) + a + (2,) + e = (1,).count(1) + f = 1, *range(10) + g = 1, *"ten" +-what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove) +-what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove) +-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all() +-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all() ++what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( ++ vars_to_remove ++) ++what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( ++ vars_to_remove ++) ++result = ( ++ session.query(models.Customer.id) ++ .filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ) ++ .order_by(models.Customer.id.asc()) ++ .all() ++) ++result = ( ++ session.query(models.Customer.id) ++ .filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ) ++ .order_by(models.Customer.id.asc()) ++ .all() ++) + Ø = set() + authors.łukasz.say_thanks() + mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), + } + ++ + def gen(): + yield from outside_of_generator +- a = (yield) +- b = ((yield)) +- c = (((yield))) ++ a = yield ++ b = yield ++ c = yield ++ + + async def f(): + await some.complicated[0].call(with_args=(True or (1 is not 1))) +-print(* [] or [1]) ++ ++ ++print(*[] or [1]) + print(**{1: 3} if False else {x: x for x in range(3)}) +-print(* lambda x: x) +-assert(not Test),("Short message") +-assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" +-assert(((parens is TooMany))) +-for x, in (1,), (2,), (3,): ... +-for y in (): ... +-for z in (i for i in (1, 2, 3)): ... +-for i in (call()): ... +-for j in (1 + (2 + 3)): ... +-while(this and that): ... +-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): ++print(*lambda x: x) ++assert not Test, "Short message" ++assert this is ComplexTest and not requirements.fit_in_a_single_line( ++ force=False ++), "Short message" ++assert parens is TooMany ++for (x,) in (1,), (2,), (3,): ++ ... ++for y in (): ++ ... ++for z in (i for i in (1, 2, 3)): ++ ... ++for i in call(): ++ ... ++for j in 1 + (2 + 3): ++ ... ++while this and that: ++ ... ++for ( ++ addr_family, ++ addr_type, ++ addr_proto, ++ addr_canonname, ++ addr_sockaddr, ++) in socket.getaddrinfo("google.com", "http"): + pass +-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +-if ( +- threading.current_thread() != threading.main_thread() and +- threading.current_thread() != threading.main_thread() or +- signal.getsignal(signal.SIGINT) != signal.default_int_handler +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- return True +-if ( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa / +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-): +- 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 +-): +- 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 +-): +- return True +-if ( +- ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ++a = ( ++ aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp ++ in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ++) ++a = ( ++ aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp ++ not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ++) ++a = ( ++ aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp ++ is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ++) ++a = ( ++ aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp ++ is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz ++) ++if ( ++ threading.current_thread() != threading.main_thread() ++ and threading.current_thread() != threading.main_thread() ++ or signal.getsignal(signal.SIGINT) != signal.default_int_handler ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ return True ++if ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ / aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++): ++ 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 ++): ++ 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 ++): ++ return True ++if ( ++ ~aaaaaaaaaaaaaaaa.a ++ + aaaaaaaaaaaaaaaa.b ++ - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e ++ | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ++ ^ aaaaaaaaaaaaaaaa.i ++ << aaaaaaaaaaaaaaaa.k ++ >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n + ): + return True + last_call() + # standalone comment at ENDMARKER diff --git a/tests/test_black.py b/tests/test_black.py index a688c8780ef..28b75787bd6 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -395,6 +395,31 @@ def test_numeric_literals_ignoring_underscores(self) -> None: 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") + 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" + ) + try: + result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)]) + self.assertEqual(result.exit_code, 0) + finally: + os.unlink(tmp_file) + actual = result.output + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + actual = actual.rstrip() + "\n" # the diff output has a trailing space + if expected != actual: + dump = black.dump_to_file(actual) + 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}" + ) + self.assertEqual(expected, actual, msg) + @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: source, expected = read_data("python2_print_function") From 71117e730c4f62458b30af820f51890487b458e4 Mon Sep 17 00:00:00 2001 From: Oliver Newman <15459200+orn688@users.noreply.github.com> Date: Wed, 27 Jan 2021 12:36:21 -0500 Subject: [PATCH 118/680] Update example exclude to match only files in root (#1861) * Update example exclude to match only files in root The `exclude` section of the example `pyproject.toml` file didn't work as expected. It claimed to exclude matched files only in the project root, but it actually excluded matched files at any directory level within the project. We can address this by prepending `^/` to the regex to ensure that it only matches files in the project root. See https://github.com/psf/black/issues/1473#issuecomment-740008873 for explanation. * Mention excluding directories as well --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1ec76938f0..83ecb89d6c9 100644 --- a/README.md +++ b/README.md @@ -313,9 +313,10 @@ line-length = 88 target-version = ['py37'] include = '\.pyi?$' exclude = ''' - -( - /( +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/( + ( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg From 988c686d312986760a42cc7edd9a79a8f31c4769 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 1 Feb 2021 17:54:19 +0000 Subject: [PATCH 119/680] Remove placeholder exit code in unreachable 'black-primer' subprocess handler (#1952) --- src/black_primer/lib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index c3069812b22..39ae93ba516 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -58,13 +58,16 @@ async def _gen_check_output( await process.wait() raise - if process.returncode != 0: - returncode = process.returncode - if returncode is None: - returncode = 69 + # 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(returncode, cmd_str, output=stdout, stderr=stderr) + raise CalledProcessError( + process.returncode, cmd_str, output=stdout, stderr=stderr + ) return (stdout, stderr) From df4dd38a9a2b44c14485948fe8209fa19db2383a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Feb 2021 15:54:14 -0800 Subject: [PATCH 120/680] Bump bleach from 3.2.1 to 3.3.0 (#1957) Bumps [bleach](https://github.com/mozilla/bleach) from 3.2.1 to 3.3.0. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v3.2.1...v3.3.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 219 ++++++++++++++++++++++----------------------------- 1 file changed, 92 insertions(+), 127 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 91fd3a49556..44e987de0e4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -78,7 +78,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -86,7 +85,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "black": { @@ -117,17 +115,14 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "python_version <": "3.7", - "version": "==0.6", - "version >": "0.6" + "version": "==0.6" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "version": "==3.1" }, "multidict": { "hashes": [ @@ -169,7 +164,6 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy-extensions": { @@ -235,20 +229,6 @@ "index": "pypi", "version": "==2020.11.13" }, - "setuptools-scm": { - "hashes": [ - "sha256:1fc4e25df445351d172bb4788f4d07f9e9ce0e8b7dee6b19584e46110172ca13", - "sha256:48b31488d089270500f120efea723968c01abd85fd4876043a3b7c7ef7d0b761", - "sha256:62fa535edb31ece9fa65dc9dcb3056145b8020c8c26c0ef1018aef33db95c40d", - "sha256:b928021c4381f96d577847d540d6e03065f8f8851c768a0c9bc552d463bae0d4", - "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a", - "sha256:db4ab2e0c2644ba71b1e5212b14ff65dbf0af465796d314a75e0cf6128f605f7", - "sha256:e878036c2527dfd05818727846338be86b2be6955741b85fab2625f63d001021", - "sha256:eb19b46816fc106a4c2d4180022687eab40f9773cf61390b845afb093d1f4ecd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.0.1" - }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -300,9 +280,7 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "python_version <": "3.8", - "version": "==3.7.4.3", - "version >=": "3.7.4" + "version": "==3.7.4.3" }, "yarl": { "hashes": [ @@ -344,7 +322,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -420,7 +397,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -428,7 +404,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "babel": { @@ -436,7 +411,6 @@ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "black": { @@ -448,11 +422,11 @@ }, "bleach": { "hashes": [ - "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", - "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" + "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", + "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.2.1" + "index": "pypi", + "version": "==3.3.0" }, "certifi": { "hashes": [ @@ -479,6 +453,7 @@ "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", + "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e", "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", @@ -507,7 +482,6 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -530,7 +504,6 @@ "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" }, "commonmark": { @@ -612,7 +585,6 @@ "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==3.3.1" }, "distlib": { @@ -625,7 +597,6 @@ "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", - "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -656,26 +627,23 @@ }, "identify": { "hashes": [ - "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", - "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e" + "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66", + "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.10" + "version": "==1.5.13" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "version": "==3.1" }, "imagesize": { "hashes": [ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "jeepney": { @@ -688,19 +656,17 @@ }, "jinja2": { "hashes": [ - "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", - "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.2" + "version": "==2.11.3" }, "keyring": { "hashes": [ - "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675", - "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea" + "sha256:9acb3e1452edbb7544822b12fd25459078769e560fa51f418b6d00afaa6178df", + "sha256:9f44660a5d4931bdc14c08a1d01ef30b18a7a8147380710d8c9f9531e1f6c3c0" ], - "markers": "python_version >= '3.6'", - "version": "==21.8.0" + "version": "==22.0.1" }, "markupsafe": { "hashes": [ @@ -709,8 +675,12 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -719,26 +689,40 @@ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -788,7 +772,6 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy": { @@ -828,11 +811,10 @@ }, "packaging": { "hashes": [ - "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", - "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.8" + "version": "==20.9" }, "pathspec": { "hashes": [ @@ -844,10 +826,10 @@ }, "pkginfo": { "hashes": [ - "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", - "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" + "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", + "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" ], - "version": "==1.6.1" + "version": "==1.7.0" }, "pre-commit": { "hashes": [ @@ -862,7 +844,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pycparser": { @@ -870,7 +851,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -878,49 +858,54 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { "hashes": [ - "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", - "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" + "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", + "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.3" + "version": "==2.7.4" }, "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" }, "pytz": { "hashes": [ - "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", - "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2020.5" + "version": "==2021.1" }, "pyyaml": { "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" - ], - "version": "==5.3.1" + "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:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + ], + "version": "==5.4.1" }, "readme-renderer": { "hashes": [ @@ -990,7 +975,6 @@ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.25.1" }, "requests-toolbelt": { @@ -1017,16 +1001,10 @@ }, "setuptools-scm": { "hashes": [ - "sha256:1fc4e25df445351d172bb4788f4d07f9e9ce0e8b7dee6b19584e46110172ca13", - "sha256:48b31488d089270500f120efea723968c01abd85fd4876043a3b7c7ef7d0b761", "sha256:62fa535edb31ece9fa65dc9dcb3056145b8020c8c26c0ef1018aef33db95c40d", - "sha256:b928021c4381f96d577847d540d6e03065f8f8851c768a0c9bc552d463bae0d4", - "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a", - "sha256:db4ab2e0c2644ba71b1e5212b14ff65dbf0af465796d314a75e0cf6128f605f7", - "sha256:e878036c2527dfd05818727846338be86b2be6955741b85fab2625f63d001021", - "sha256:eb19b46816fc106a4c2d4180022687eab40f9773cf61390b845afb093d1f4ecd" + "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "index": "pypi", "version": "==5.0.1" }, "six": { @@ -1034,15 +1012,14 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "sphinx": { "hashes": [ @@ -1057,7 +1034,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -1065,7 +1041,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -1073,7 +1048,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -1081,7 +1055,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -1089,7 +1062,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -1097,7 +1069,6 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { @@ -1110,11 +1081,10 @@ }, "tqdm": { "hashes": [ - "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416", - "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208" + "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a", + "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.55.0" + "version": "==4.56.0" }, "twine": { "hashes": [ @@ -1167,25 +1137,21 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "python_version <": "3.8", - "version": "==3.7.4.3", - "version >=": "3.7.4" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "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.2" + "version": "==1.26.3" }, "virtualenv": { "hashes": [ - "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", - "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" + "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", + "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.2" + "version": "==20.4.2" }, "webencodings": { "hashes": [ @@ -1242,7 +1208,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" } } From 3fca540d05a0f0b4480cfcce84d7e0392270b841 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Feb 2021 13:03:42 -0800 Subject: [PATCH 121/680] speed up cache by approximately 42x by avoiding pathlib (#1953) --- CHANGES.md | 2 ++ src/black/__init__.py | 14 +++++++++----- tests/test_black.py | 39 +++++++++++++++++++++------------------ 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ca8a0472a3b..5f7ca4f4da7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - use lowercase hex strings (#1692) +- speed up caching by avoiding pathlib (#1950) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/src/black/__init__.py b/src/black/__init__.py index 9034bf6cf77..7c1a013aae3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -93,7 +93,7 @@ Timestamp = float FileSize = int CacheInfo = Tuple[Timestamp, FileSize] -Cache = Dict[Path, CacheInfo] +Cache = Dict[str, CacheInfo] out = partial(click.secho, bold=True, err=True) err = partial(click.secho, fg="red", err=True) @@ -724,7 +724,8 @@ def reformat_one( if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): cache = read_cache(mode) res_src = src.resolve() - if res_src in cache and cache[res_src] == get_cache_info(res_src): + res_src_s = str(res_src) + if res_src_s in cache and cache[res_src_s] == get_cache_info(res_src): changed = Changed.CACHED if changed is not Changed.CACHED and format_file_in_place( src, fast=fast, write_back=write_back, mode=mode @@ -6781,8 +6782,8 @@ def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set """ todo, done = set(), set() for src in sources: - src = src.resolve() - if cache.get(src) != get_cache_info(src): + res_src = src.resolve() + if cache.get(str(res_src)) != get_cache_info(res_src): todo.add(src) else: done.add(src) @@ -6794,7 +6795,10 @@ def write_cache(cache: Cache, sources: Iterable[Path], mode: Mode) -> None: cache_file = get_cache_file(mode) try: CACHE_DIR.mkdir(parents=True, exist_ok=True) - new_cache = {**cache, **{src.resolve(): get_cache_info(src) for src in sources}} + 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) diff --git a/tests/test_black.py b/tests/test_black.py index 28b75787bd6..cfd3cbd91b3 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1003,7 +1003,7 @@ def test_cache_broken_file(self) -> None: fobj.write("print('hello')") self.invokeBlack([str(src)]) cache = black.read_cache(mode) - self.assertIn(src, cache) + self.assertIn(str(src), cache) def test_cache_single_file_already_cached(self) -> None: mode = DEFAULT_MODE @@ -1035,8 +1035,8 @@ def test_cache_multiple_files(self) -> None: with two.open("r") as fobj: self.assertEqual(fobj.read(), 'print("hello")\n') cache = black.read_cache(mode) - self.assertIn(one, cache) - self.assertIn(two, cache) + self.assertIn(str(one), cache) + self.assertIn(str(two), cache) def test_no_cache_when_writeback_diff(self) -> None: mode = DEFAULT_MODE @@ -1116,8 +1116,8 @@ def test_write_cache_read_cache(self) -> None: src.touch() black.write_cache({}, [src], mode) cache = black.read_cache(mode) - self.assertIn(src, cache) - self.assertEqual(cache[src], black.get_cache_info(src)) + 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: @@ -1128,7 +1128,10 @@ def test_filter_cached(self) -> None: uncached.touch() cached.touch() cached_but_changed.touch() - cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)} + 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} ) @@ -1156,8 +1159,8 @@ def test_failed_formatting_does_not_get_cached(self) -> None: fobj.write('print("hello")\n') self.invokeBlack([str(workspace)], exit_code=123) cache = black.read_cache(mode) - self.assertNotIn(failing, cache) - self.assertIn(clean, cache) + self.assertNotIn(str(failing), cache) + self.assertIn(str(clean), cache) def test_write_cache_write_fail(self) -> None: mode = DEFAULT_MODE @@ -1210,9 +1213,9 @@ def test_read_cache_line_lengths(self) -> None: path.touch() black.write_cache({}, [path], mode) one = black.read_cache(mode) - self.assertIn(path, one) + self.assertIn(str(path), one) two = black.read_cache(short_mode) - self.assertNotIn(path, two) + self.assertNotIn(str(path), two) def test_single_file_force_pyi(self) -> None: pyi_mode = replace(DEFAULT_MODE, is_pyi=True) @@ -1226,9 +1229,9 @@ def test_single_file_force_pyi(self) -> None: actual = fh.read() # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) - self.assertIn(path, pyi_cache) + self.assertIn(str(path), pyi_cache) normal_cache = black.read_cache(DEFAULT_MODE) - self.assertNotIn(path, normal_cache) + self.assertNotIn(str(path), normal_cache) self.assertFormatEqual(expected, actual) black.assert_equivalent(contents, actual) black.assert_stable(contents, actual, pyi_mode) @@ -1255,8 +1258,8 @@ def test_multi_file_force_pyi(self) -> None: pyi_cache = black.read_cache(pyi_mode) normal_cache = black.read_cache(reg_mode) for path in paths: - self.assertIn(path, pyi_cache) - self.assertNotIn(path, normal_cache) + self.assertIn(str(path), pyi_cache) + self.assertNotIn(str(path), normal_cache) def test_pipe_force_pyi(self) -> None: source, expected = read_data("force_pyi") @@ -1280,9 +1283,9 @@ def test_single_file_force_py36(self) -> None: actual = fh.read() # verify cache with --target-version is separate py36_cache = black.read_cache(py36_mode) - self.assertIn(path, py36_cache) + self.assertIn(str(path), py36_cache) normal_cache = black.read_cache(reg_mode) - self.assertNotIn(path, normal_cache) + self.assertNotIn(str(path), normal_cache) self.assertEqual(actual, expected) @event_loop() @@ -1307,8 +1310,8 @@ def test_multi_file_force_py36(self) -> None: pyi_cache = black.read_cache(py36_mode) normal_cache = black.read_cache(reg_mode) for path in paths: - self.assertIn(path, pyi_cache) - self.assertNotIn(path, normal_cache) + self.assertIn(str(path), pyi_cache) + self.assertNotIn(str(path), normal_cache) def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") From aebd3c37b28bbc0183a58d13b80e7595db3c09bb Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 4 Feb 2021 23:10:45 -0500 Subject: [PATCH 122/680] Update prettier in pre-commit config (#1966) With older versions of prettier, when the hook failed a bunch of "[error] No parser could be inferred for file: {PATH}" error lines showed up because of lack of support of a flag that pre-commit passes for us by default. It made figuring out why the prettier hook failed annoying. --- .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 9955a24baf8..292ed5727d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: exclude: ^docs/conf.py - repo: https://github.com/pre-commit/mirrors-prettier - rev: v1.19.1 + rev: v2.2.1 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] From 1c364f42ee298251d009e39fcab5ecc8cd0384d6 Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 10 Feb 2021 01:10:02 +0000 Subject: [PATCH 123/680] Regenerate documentation (#1980) Resolves #1979 and ensures that the content from #1861 is included in the repository-published documentation. --- docs/installation_and_usage.md | 5 ----- docs/pyproject_toml.md | 7 ++++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index e3b53fd076e..ee45c934da8 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -100,11 +100,6 @@ Options: respect --force-exclude option on some editors that rely on using stdin. - --stdin-filename TEXT 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. - -q, --quiet Don't emit non-error messages to stderr. Errors are still emitted; silence those with 2>/dev/null. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index cd313452b1e..453f533bf96 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -55,9 +55,10 @@ line-length = 88 target-version = ['py37'] include = '\.pyi?$' exclude = ''' - -( - /( +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/( + ( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg From 2d0c14989dca41676fc83fb36f2d652cf93fad58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Feb 2021 18:17:23 -0800 Subject: [PATCH 124/680] Bump cryptography from 3.3.1 to 3.3.2 (#1981) Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.3.1...3.3.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 44e987de0e4..9aa2d0e7ce8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -425,7 +425,6 @@ "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], - "index": "pypi", "version": "==3.3.0" }, "certifi": { @@ -570,22 +569,23 @@ }, "cryptography": { "hashes": [ - "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", - "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7", - "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901", - "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c", - "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244", - "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6", - "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5", - "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e", - "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c", - "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0", - "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812", - "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a", - "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", - "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" + "sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72", + "sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff", + "sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c", + "sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3", + "sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed", + "sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed", + "sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433", + "sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e", + "sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44", + "sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed", + "sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042", + "sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b", + "sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f", + "sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da" ], - "version": "==3.3.1" + "index": "pypi", + "version": "==3.3.2" }, "distlib": { "hashes": [ @@ -993,11 +993,11 @@ }, "secretstorage": { "hashes": [ - "sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa", - "sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f" + "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", + "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" ], "markers": "sys_platform == 'linux'", - "version": "==3.3.0" + "version": "==3.3.1" }, "setuptools-scm": { "hashes": [ @@ -1081,10 +1081,10 @@ }, "tqdm": { "hashes": [ - "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a", - "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65" + "sha256:2874fa525c051177583ec59c0fb4583e91f28ccd3f217ffad2acdb32d2c789ac", + "sha256:ab9b659241d82b8b51b2269ee243ec95286046bf06015c4e15a947cc15914211" ], - "version": "==4.56.0" + "version": "==4.56.1" }, "twine": { "hashes": [ From b8c1020b526b488a1a216d9a4d58fc8616b61a99 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 11 Feb 2021 20:11:42 +0000 Subject: [PATCH 125/680] Stability fixup: interaction between newlines and comments (#1975) * Add test case to illustrate the issue * Accept carriage returns as valid separators while enumerating comments Without this acceptance, escaped multi-line statments that use carriage returns will not be counted into the 'ignored_lines' variable since the emitted line values will end with a CR and not an escape character. That leads to comments associated with the line being incorrectly labeled with the STANDALONE_COMMENT type, affecting comment placement and line space management. * Remove comment linking to ephemeral build log --- src/black/__init__.py | 2 +- tests/test_black.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 7c1a013aae3..6dbb765cf7a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2635,7 +2635,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: consumed = 0 nlines = 0 ignored_lines = 0 - for index, line in enumerate(prefix.split("\n")): + for index, line in enumerate(re.split("\r?\n", prefix)): consumed += len(line) + 1 # adding the length of the split '\n' line = line.lstrip() if not line: diff --git a/tests/test_black.py b/tests/test_black.py index cfd3cbd91b3..a2efef82422 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1793,6 +1793,11 @@ def test_bpo_33660_workaround(self) -> None: finally: os.chdir(str(old_cwd)) + 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) + black.assert_stable(source, output, mode=DEFAULT_MODE) + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 76268ab0c9b4e4d030b2d16272bfbdcad5172aad Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 14 Feb 2021 10:23:47 -0500 Subject: [PATCH 126/680] Fix duplication of checks on internal PRs (#1986) Internal PRs (that come from branches of psf/black) match both the push and pull_request events. This leads to a duplication of test, lint, etc. checks on such PRs. Not only is it a waste of resources, it makes the Status Checks UI more frustrating to go through. Patch taken from: https://github.community/t/duplicate-checks-on-push-and-pull-request-simultaneous-event/18012 --- .github/workflows/doc.yml | 8 +++++++- .github/workflows/fuzz.yml | 7 +++++++ .github/workflows/lint.yml | 7 +++++++ .github/workflows/primer.yml | 7 +++++++ .github/workflows/test.yml | 7 +++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index d266e55aa02..930b6d440ff 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -16,8 +16,14 @@ on: jobs: build: - runs-on: ubuntu-latest + # 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: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 0153767509a..f3b688f3ef6 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -4,6 +4,13 @@ on: [push, pull_request] 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: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fa7286eec1f..e01e9ade5e2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,6 +4,13 @@ on: [push, pull_request] 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: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index b623b938345..4c5751ae996 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -4,6 +4,13 @@ on: [push, pull_request] 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8dd4e4f59fd..bec769064c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,13 @@ on: [push, pull_request] 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 From 3a309a36fcfc30a96a7fdd68b6b2c3aa2aa5341c Mon Sep 17 00:00:00 2001 From: rht Date: Sun, 14 Feb 2021 10:32:29 -0500 Subject: [PATCH 127/680] readme: Include Zulip in used-by section (#1987) Zulip has recently formatted their code in Black and also set Black as a linter: https://github.com/zulip/zulip/pull/15662. See also https://chat.zulip.org/#narrow/stream/2-general/topic/black. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83ecb89d6c9..97df938eab2 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,7 @@ then write the above files to `.cache/black//`. 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, -every Datadog Agent Integration, Home Assistant. +every Datadog Agent Integration, Home Assistant, Zulip. The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora. From 6a105e019fcaa10cc0b6fb59edea4a4018eac97e Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Mon, 15 Feb 2021 18:02:48 +0200 Subject: [PATCH 128/680] Add "# fmt: skip" directive to black (#1800) Fixes #1162 --- README.md | 5 +- src/black/__init__.py | 104 +++++++++++++++++++++++++---------------- tests/data/fmtskip.py | 3 ++ tests/data/fmtskip2.py | 17 +++++++ tests/data/fmtskip3.py | 20 ++++++++ tests/data/fmtskip4.py | 13 ++++++ tests/data/fmtskip5.py | 22 +++++++++ tests/test_format.py | 5 ++ 8 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 tests/data/fmtskip.py create mode 100644 tests/data/fmtskip2.py create mode 100644 tests/data/fmtskip3.py create mode 100644 tests/data/fmtskip4.py create mode 100644 tests/data/fmtskip5.py diff --git a/README.md b/README.md index 97df938eab2..89ef4271d41 100644 --- a/README.md +++ b/README.md @@ -239,8 +239,9 @@ feeling confident, use `--fast`. _Black_ is a PEP 8 compliant opinionated formatter. _Black_ reformats entire files in place. It is not configurable. It doesn't take previous formatting into account. Your main option of configuring _Black_ is that it doesn't reformat blocks that start with -`# fmt: off` and end with `# fmt: on`. `# fmt: on/off` have to be on the same level of -indentation. To learn more about _Black_'s opinions, to go +`# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`. Pay +attention that `# fmt: on/off` have to be on the same level of indentation. To learn +more about _Black_'s opinions, to go [the_black_code_style](https://github.com/psf/black/blob/master/docs/the_black_code_style.md). Please refer to this document before submitting an issue. What seems like a bug might be diff --git a/src/black/__init__.py b/src/black/__init__.py index 6dbb765cf7a..08930d11cea 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2581,6 +2581,8 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr 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"} @@ -5404,58 +5406,80 @@ 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 in FMT_OFF: - # 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.type != STANDALONE_COMMENT: - prev = preceding_leaf(leaf) - if prev and prev.type not in WHITESPACE: + if comment.value not in FMT_PASS: + 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: + 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: continue - ignored_nodes = list(generate_ignored_nodes(leaf)) - if not ignored_nodes: - continue - - first = ignored_nodes[0] # Can be a container node with the `leaf`. - parent = first.parent - prefix = first.prefix - first.prefix = prefix[comment.consumed :] - hidden_value = ( - comment.value + "\n" + "".join(str(n) for n in ignored_nodes) - ) - if hidden_value.endswith("\n"): - # That happens when one of the `ignored_nodes` ended with a NEWLINE - # leaf (possibly followed by a DEDENT). - hidden_value = hidden_value[:-1] - first_idx: Optional[int] = None - for ignored in ignored_nodes: - index = ignored.remove() - if first_idx is None: - first_idx = index - assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" - assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" - parent.insert_child( - first_idx, - Leaf( - STANDALONE_COMMENT, - hidden_value, - prefix=prefix[:previous_consumed] + "\n" * comment.newlines, - ), - ) - return True + ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + if not ignored_nodes: + continue - previous_consumed = comment.consumed + first = ignored_nodes[0] # Can be a container node with the `leaf`. + parent = first.parent + prefix = first.prefix + first.prefix = prefix[comment.consumed :] + 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: + hidden_value += " " + 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). + hidden_value = hidden_value[:-1] + first_idx: Optional[int] = None + for ignored in ignored_nodes: + index = ignored.remove() + if first_idx is None: + first_idx = index + assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" + assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" + parent.insert_child( + first_idx, + Leaf( + STANDALONE_COMMENT, + hidden_value, + prefix=prefix[:previous_consumed] + "\n" * comment.newlines, + ), + ) + return True return False -def generate_ignored_nodes(leaf: Leaf) -> 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. """ 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, "") + 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 + return while container is not None and container.type != token.ENDMARKER: if is_fmt_on(container): return diff --git a/tests/data/fmtskip.py b/tests/data/fmtskip.py new file mode 100644 index 00000000000..1d5836fc031 --- /dev/null +++ b/tests/data/fmtskip.py @@ -0,0 +1,3 @@ +a, b = 1, 2 +c = 6 # fmt: skip +d = 5 diff --git a/tests/data/fmtskip2.py b/tests/data/fmtskip2.py new file mode 100644 index 00000000000..e6248117aa9 --- /dev/null +++ b/tests/data/fmtskip2.py @@ -0,0 +1,17 @@ +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 + +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", +] \ No newline at end of file diff --git a/tests/data/fmtskip3.py b/tests/data/fmtskip3.py new file mode 100644 index 00000000000..6e166888e21 --- /dev/null +++ b/tests/data/fmtskip3.py @@ -0,0 +1,20 @@ +a = 3 +# fmt: off +b, c = 1, 2 +d = 6 # fmt: skip +e = 5 +# fmt: on +f = ["This is a very long line that should be formatted into a clearer line ", "by rearranging."] + +# output + +a = 3 +# fmt: off +b, c = 1, 2 +d = 6 # fmt: skip +e = 5 +# fmt: on +f = [ + "This is a very long line that should be formatted into a clearer line ", + "by rearranging.", +] diff --git a/tests/data/fmtskip4.py b/tests/data/fmtskip4.py new file mode 100644 index 00000000000..aadd77d0e53 --- /dev/null +++ b/tests/data/fmtskip4.py @@ -0,0 +1,13 @@ +a = 2 +# fmt: skip +l = [1, 2, 3,] + +# output + +a = 2 +# fmt: skip +l = [ + 1, + 2, + 3, +] \ No newline at end of file diff --git a/tests/data/fmtskip5.py b/tests/data/fmtskip5.py new file mode 100644 index 00000000000..d7b15e0ff41 --- /dev/null +++ b/tests/data/fmtskip5.py @@ -0,0 +1,22 @@ +a, b, c = 3, 4, 5 +if ( + a == 3 + and b != 9 # fmt: skip + and c is not None +): + print("I'm good!") +else: + print("I'm bad") + + +# output + +a, b, c = 3, 4, 5 +if ( + a == 3 + and b != 9 # fmt: skip + and c is not None +): + print("I'm good!") +else: + print("I'm bad") diff --git a/tests/test_format.py b/tests/test_format.py index e4677707e3c..e0cb0b74195 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -37,6 +37,11 @@ "fmtonoff2", "fmtonoff3", "fmtonoff4", + "fmtskip", + "fmtskip2", + "fmtskip3", + "fmtskip4", + "fmtskip5", "fstring", "function", "function2", From 71bbb678b9f3ad1b18eab9c967b39f22a0d19535 Mon Sep 17 00:00:00 2001 From: Archit Gopal <73956153+Architrixs@users.noreply.github.com> Date: Mon, 15 Feb 2021 21:35:23 +0530 Subject: [PATCH 129/680] add gedit integration (#1988) --- docs/editor_integration.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index 037b265b9a0..aa3a1eeedec 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -233,6 +233,36 @@ If you later want to update _Black_, you should do it like this: $ pip install -U black --no-binary regex,typed-ast ``` +## Gedit + +gedit is the default text editor of the GNOME, Unix like Operating Systems. Open gedit +as + +```console +$ gedit +``` + +1. `Go to edit > preferences > plugins` +2. Search for `external tools` and activate it. +3. In `Tools menu -> Manage external tools` +4. Add a new tool using `+` button. +5. Copy the below content to the code window. + +```console +#!/bin/bash +Name=$GEDIT_CURRENT_DOCUMENT_NAME +black $Name +``` + +- Set a keyboard shortcut if you like, Ex. `ctrl-B` +- Save: `Nothing` +- Input: `Nothing` +- Output: `Display in bottom pane` if you like. +- Change the name of the tool if you like. + +Use your keyboard shortcut or `Tools -> External Tools` to use your new tool. When you +close and reopen your File, _Black_ will be done with its job. + ## Visual Studio Code Use the From 97b8496d67741a10f30eefbb3d6c97a0e541fba4 Mon Sep 17 00:00:00 2001 From: Romain Rigaux Date: Tue, 16 Feb 2021 13:10:23 -0800 Subject: [PATCH 130/680] Use 'args' to Avoid GH workflow warning (#1990) Unexpected input(s) 'black_args', valid inputs are ['entryPoint', 'args'] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89ef4271d41..178f763c32d 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ jobs: - uses: actions/setup-python@v2 - uses: psf/black@stable with: - black_args: ". --check" + args: ". --check" ``` ### Inputs From 306a513137ec93a5053d415319e24bd4057d4045 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sat, 20 Feb 2021 16:44:48 +0000 Subject: [PATCH 131/680] fuzzer: add special-case for multi-line EOF TokenError (#1991) * Add special-case for multi-line EOF TokenError under Python3.7 * Update conditional check to correspond to original issue description (and include issue hyperlink) * Add warning and hint regarding replaying the fuzzer code generation * Include code review suggestion (with modifications for this to follow) Co-authored-by: Zac Hatfield-Dodds * Remove Python version check, since this issue does exist across more recent Python versions than 3.7 * Update explanatory comment * Add clarification for potentially-ambiguous blib2to3 import * Update explanatory comment * Bring comment into consistent format with previous comment * Revert "Add clarification for potentially-ambiguous blib2to3 import" This reverts commit 357b7cc03bdb19dd924f1accd428352f4bb44e5c. Co-authored-by: Zac Hatfield-Dodds --- fuzz.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fuzz.py b/fuzz.py index 6330c58cc81..6e2068690b5 100644 --- a/fuzz.py +++ b/fuzz.py @@ -5,10 +5,13 @@ a coverage-guided fuzzer I'm working on. """ +import re + import hypothesmith from hypothesis import HealthCheck, given, settings, strategies as st import black +from blib2to3.pgen2.tokenize import TokenError # This test uses the Hypothesis and Hypothesmith libraries to generate random @@ -46,6 +49,16 @@ def test_idempotent_any_syntatically_valid_python( # able to cope with it. See issues #970, #1012, #1358, and #1557. # TODO: remove this try-except block when issues are resolved. return + except TokenError as e: + if ( + e.args[0] == "EOF in multi-line statement" + and re.search(r"\r?\n\\\r?\n", src_contents) is not None + ): + # This is a bug - if it's valid Python code, as above, Black should be + # able to cope with it. See issue #1012. + # TODO: remove this block when the issue is resolved. + return + raise # And check that we got equivalent and stable output. black.assert_equivalent(src_contents, dst_contents) From afc8c326bf97ce7a22e8fc0b8cc5ced1586ef022 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:26:29 +0000 Subject: [PATCH 132/680] Brevity: rename method --- src/black/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 08930d11cea..e398ed04f43 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5005,7 +5005,7 @@ def bracket_split_build_line( result.append(leaf, preformatted=True) for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) - if is_body and should_split_body_explode(result, opening_bracket): + if is_body and should_split(result, opening_bracket): result.should_explode = True return result @@ -5810,7 +5810,7 @@ def ensure_visible(leaf: Leaf) -> None: leaf.value = ")" -def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: +def should_split(line: Line, opening_bracket: Leaf) -> bool: """Should `line` be immediately split with `delimiter_split()` after RHS?""" if not (opening_bracket.parent and opening_bracket.value in "[{("): From caa3fcc2de3cfb6162bee0bb6f68b10bc38e612f Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:30:05 +0000 Subject: [PATCH 133/680] Clarity: isolate and extract each responsibility from an overloaded variable --- src/black/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index e398ed04f43..44017b59602 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1481,6 +1481,7 @@ class Line: bracket_tracker: BracketTracker = field(default_factory=BracketTracker) inside_brackets: bool = False should_explode: bool = False + magic_trailing_comma: Optional[Leaf] = None def append(self, leaf: Leaf, preformatted: bool = False) -> None: """Add a new `leaf` to the end of the line. @@ -1508,7 +1509,7 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: self.bracket_tracker.mark(leaf) if self.mode.magic_trailing_comma: if self.has_magic_trailing_comma(leaf): - self.should_explode = True + self.magic_trailing_comma = leaf elif self.has_magic_trailing_comma(leaf, ensure_removable=True): self.remove_trailing_comma() if not self.append_comment(leaf): @@ -1792,6 +1793,7 @@ def clone(self) -> "Line": depth=self.depth, inside_brackets=self.inside_brackets, should_explode=self.should_explode, + magic_trailing_comma=self.magic_trailing_comma, ) def __str__(self) -> str: @@ -2710,7 +2712,7 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: transformers: List[Transformer] if ( not line.contains_uncollapsable_type_comments() - and not line.should_explode + and not (line.should_explode or line.magic_trailing_comma) and ( is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() @@ -4385,6 +4387,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: depth=line.depth + 1, inside_brackets=True, should_explode=line.should_explode, + magic_trailing_comma=line.magic_trailing_comma, ) string_leaf = Leaf(token.STRING, string_value) insert_str_child(string_leaf) @@ -5946,7 +5949,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf """ omit: Set[LeafID] = set() - if not line.should_explode: + if not line.should_explode and not line.magic_trailing_comma: yield omit length = 4 * line.depth @@ -5968,7 +5971,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf elif leaf.type in CLOSING_BRACKETS: prev = line.leaves[index - 1] if index > 0 else None if ( - line.should_explode + (line.should_explode or line.magic_trailing_comma) and prev and prev.type == token.COMMA and not is_one_tuple_between( @@ -5996,7 +5999,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf yield omit if ( - line.should_explode + (line.should_explode or line.magic_trailing_comma) and prev and prev.type == token.COMMA and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) @@ -6629,7 +6632,7 @@ def can_omit_invisible_parens( penultimate = line.leaves[-2] last = line.leaves[-1] - if line.should_explode: + if line.should_explode or line.magic_trailing_comma: try: penultimate, last = last_two_except(line.leaves, omit=omit_on_explode) except LookupError: @@ -6656,7 +6659,9 @@ def can_omit_invisible_parens( # unnecessary. return True - if line.should_explode and penultimate.type == token.COMMA: + if ( + line.should_explode or 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 From a23f521fee1b283b10000d94267ef93069b7f7ec Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:37:25 +0000 Subject: [PATCH 134/680] Brevity: only use the variables required to convey the intended expressions --- src/black/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 44017b59602..67eef95390d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5971,7 +5971,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf elif leaf.type in CLOSING_BRACKETS: prev = line.leaves[index - 1] if index > 0 else None if ( - (line.should_explode or line.magic_trailing_comma) + line.magic_trailing_comma and prev and prev.type == token.COMMA and not is_one_tuple_between( @@ -5999,7 +5999,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf yield omit if ( - (line.should_explode or line.magic_trailing_comma) + line.magic_trailing_comma and prev and prev.type == token.COMMA and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) @@ -6659,9 +6659,7 @@ def can_omit_invisible_parens( # unnecessary. return True - if ( - line.should_explode or line.magic_trailing_comma - ) and penultimate.type == token.COMMA: + 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 From 51141f1af4aab0e0c7f71932ffe06482a884f1d5 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:38:55 +0000 Subject: [PATCH 135/680] Consistency: use variable names that correspond to the methods they invoke --- src/black/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 67eef95390d..c836b2b46eb 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1480,7 +1480,7 @@ class Line: comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict) bracket_tracker: BracketTracker = field(default_factory=BracketTracker) inside_brackets: bool = False - should_explode: bool = False + should_split: bool = False magic_trailing_comma: Optional[Leaf] = None def append(self, leaf: Leaf, preformatted: bool = False) -> None: @@ -1792,7 +1792,7 @@ def clone(self) -> "Line": mode=self.mode, depth=self.depth, inside_brackets=self.inside_brackets, - should_explode=self.should_explode, + should_split=self.should_split, magic_trailing_comma=self.magic_trailing_comma, ) @@ -2712,7 +2712,7 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: transformers: List[Transformer] if ( not line.contains_uncollapsable_type_comments() - and not (line.should_explode or line.magic_trailing_comma) + and not (line.should_split or line.magic_trailing_comma) and ( is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() @@ -4386,7 +4386,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: mode=line.mode, depth=line.depth + 1, inside_brackets=True, - should_explode=line.should_explode, + should_split=line.should_split, magic_trailing_comma=line.magic_trailing_comma, ) string_leaf = Leaf(token.STRING, string_value) @@ -5009,7 +5009,7 @@ def bracket_split_build_line( for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) if is_body and should_split(result, opening_bracket): - result.should_explode = True + result.should_split = True return result @@ -5949,7 +5949,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf """ omit: Set[LeafID] = set() - if not line.should_explode and not line.magic_trailing_comma: + if not line.should_split and not line.magic_trailing_comma: yield omit length = 4 * line.depth @@ -6632,7 +6632,7 @@ def can_omit_invisible_parens( penultimate = line.leaves[-2] last = line.leaves[-1] - if line.should_explode or line.magic_trailing_comma: + if line.should_split or line.magic_trailing_comma: try: penultimate, last = last_two_except(line.leaves, omit=omit_on_explode) except LookupError: From 89c42a0011721e164738c17a192452ee77d9cc99 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:40:42 +0000 Subject: [PATCH 136/680] Clarity: special case: avoid using variables that have the same names as methods --- src/black/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index c836b2b46eb..7f727b18bf0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5008,7 +5008,7 @@ def bracket_split_build_line( result.append(leaf, preformatted=True) for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) - if is_body and should_split(result, opening_bracket): + if is_body and should_split_line(result, opening_bracket): result.should_split = True return result @@ -5813,7 +5813,7 @@ def ensure_visible(leaf: Leaf) -> None: leaf.value = ")" -def should_split(line: Line, opening_bracket: Leaf) -> bool: +def should_split_line(line: Line, opening_bracket: Leaf) -> bool: """Should `line` be immediately split with `delimiter_split()` after RHS?""" if not (opening_bracket.parent and opening_bracket.value in "[{("): From 829331a8777aa758c6fc2a5032d38d8fbb5b5ac6 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:45:45 +0000 Subject: [PATCH 137/680] Simplification: only use special-case token retrieval logic when magic trailing comma is present --- 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 7f727b18bf0..56f180ac8a6 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6632,7 +6632,7 @@ def can_omit_invisible_parens( penultimate = line.leaves[-2] last = line.leaves[-1] - if line.should_split or line.magic_trailing_comma: + if line.magic_trailing_comma: try: penultimate, last = last_two_except(line.leaves, omit=omit_on_explode) except LookupError: From e0d766727dc87f5c5f39ef751d0bf23fc5ff31a0 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 17:48:38 +0000 Subject: [PATCH 138/680] Simplification: only yield empty omit list when magic trailing comma is present --- 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 56f180ac8a6..3eea33ced14 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5949,7 +5949,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf """ omit: Set[LeafID] = set() - if not line.should_split and not line.magic_trailing_comma: + if not line.magic_trailing_comma: yield omit length = 4 * line.depth From 24700806f681f2809a2b85999871e291c36dd948 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 4 Feb 2021 18:07:43 +0000 Subject: [PATCH 139/680] Cleanup: remove unused / redundant variables from conditionals --- src/black/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 3eea33ced14..c2a4bda6061 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5971,8 +5971,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf elif leaf.type in CLOSING_BRACKETS: prev = line.leaves[index - 1] if index > 0 else None if ( - line.magic_trailing_comma - and prev + prev and prev.type == token.COMMA and not is_one_tuple_between( leaf.opening_bracket, leaf, line.leaves @@ -5999,8 +5998,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf yield omit if ( - line.magic_trailing_comma - and prev + prev and prev.type == token.COMMA and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) ): @@ -6659,7 +6657,7 @@ def can_omit_invisible_parens( # unnecessary. return True - if line.magic_trailing_comma and penultimate.type == token.COMMA: + if penultimate.type == token.COMMA: # The rightmost non-omitted bracket pair is the one we want to explode on. return True From 22127c633eba10d41519fb562c1252f859e2d7fa Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 9 Feb 2021 21:13:57 +0000 Subject: [PATCH 140/680] Readability: reduce boolean nesting --- src/black/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index c2a4bda6061..c2b0ad43fa8 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2712,7 +2712,8 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: transformers: List[Transformer] if ( not line.contains_uncollapsable_type_comments() - and not (line.should_split or line.magic_trailing_comma) + and not line.should_split + and not line.magic_trailing_comma and ( is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() From 0cbe19c813559d6642e71832242264ab8d5a5d59 Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 10 Feb 2021 12:33:50 +0000 Subject: [PATCH 141/680] Minimize changes: more closely resemble original conditional logic --- 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 c2b0ad43fa8..0b734adca56 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6658,7 +6658,7 @@ def can_omit_invisible_parens( # unnecessary. return True - if penultimate.type == token.COMMA: + 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 From cd4295dd9888f491cfd9aae060d7832b7f831b24 Mon Sep 17 00:00:00 2001 From: "Paul \"TBBle\" Hampson" Date: Mon, 22 Feb 2021 17:43:23 +1100 Subject: [PATCH 142/680] Indicate that a final newline was added in --diff (#1897) (#1897) Fixes: #1662 Work-around for https://bugs.python.org/issue2142 The test has to slightly mess with its input data, because the utility functions default to ensuring the test data has a final newline, which defeats the point of the test. Signed-off-by: Paul "TBBle" Hampson --- CHANGES.md | 2 ++ docs/authors.md | 1 + src/black/__init__.py | 23 ++++++++++++++++------- tests/data/missing_final_newline.diff | 8 ++++++++ tests/data/missing_final_newline.py | 3 +++ tests/test_black.py | 23 ++++++++++++++++++++++- 6 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/data/missing_final_newline.diff create mode 100644 tests/data/missing_final_newline.py diff --git a/CHANGES.md b/CHANGES.md index 5f7ca4f4da7..3fd7ad40496 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,8 @@ - speed up caching by avoiding pathlib (#1950) +- `--diff` correctly indicates when a file doesn't end in a newline (#1662) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/docs/authors.md b/docs/authors.md index cdf5046c446..9f2ea0571b9 100644 --- a/docs/authors.md +++ b/docs/authors.md @@ -132,6 +132,7 @@ Multiple contributions by: - [Pablo Galindo](mailto:Pablogsal@gmail.com) - [Paul Ganssle](mailto:p.ganssle@gmail.com) - [Paul Meinhardt](mailto:mnhrdt@gmail.com) +- [Paul "TBBle" Hampson](mailto:Paul.Hampson@Pobox.com) - [Peter Bengtsson](mailto:mail@peterbe.com) - [Peter Grayson](mailto:pete@jpgrayson.net) - [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) diff --git a/src/black/__init__.py b/src/black/__init__.py index 0b734adca56..c1907d9b1f1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6425,14 +6425,14 @@ def assert_stable(src: str, dst: str, mode: Mode) -> None: @mypyc_attr(patchable=True) -def dump_to_file(*output: str) -> str: +def dump_to_file(*output: str, ensure_final_newline: bool = True) -> str: """Dump `output` to a temporary file. Return path to the file.""" with tempfile.NamedTemporaryFile( mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8" ) as f: for lines in output: f.write(lines) - if lines and lines[-1] != "\n": + if ensure_final_newline and lines and lines[-1] != "\n": f.write("\n") return f.name @@ -6450,11 +6450,20 @@ 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 + "\n" for line in a.splitlines()] - b_lines = [line + "\n" for line in b.splitlines()] - return "".join( - difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5) - ) + a_lines = [line for line in a.splitlines(keepends=True)] + b_lines = [line for line in b.splitlines(keepends=True)] + diff_lines = [] + for line in difflib.unified_diff( + a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 + ): + # Work around https://bugs.python.org/issue2142 + # See https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html + if line[-1] == "\n": + diff_lines.append(line) + else: + diff_lines.append(line + "\n") + diff_lines.append("\\ No newline at end of file\n") + return "".join(diff_lines) def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: diff --git a/tests/data/missing_final_newline.diff b/tests/data/missing_final_newline.diff new file mode 100644 index 00000000000..6d991c74f8f --- /dev/null +++ b/tests/data/missing_final_newline.diff @@ -0,0 +1,8 @@ +--- [Deterministic header] ++++ [Deterministic header] +@@ -1,3 +1,3 @@ + # A comment-only file, with no final EOL character + # This triggers https://bugs.python.org/issue2142 +-# This is the line without the EOL character +\ No newline at end of file ++# This is the line without the EOL character diff --git a/tests/data/missing_final_newline.py b/tests/data/missing_final_newline.py new file mode 100644 index 00000000000..687e1367552 --- /dev/null +++ b/tests/data/missing_final_newline.py @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index a2efef82422..5d14ceda8f4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -300,7 +300,6 @@ def test_expression_diff(self) -> None: os.unlink(tmp_file) actual = result.output actual = diff_header.sub(DETERMINISTIC_HEADER, actual) - actual = actual.rstrip() + "\n" # the diff output has a trailing space if expected != actual: dump = black.dump_to_file(actual) msg = ( @@ -1798,6 +1797,28 @@ def test_newline_comment_interaction(self) -> None: output = black.format_str(source, mode=DEFAULT_MODE) black.assert_stable(source, output, mode=DEFAULT_MODE) + def test_bpo_2142_workaround(self) -> None: + + # https://bugs.python.org/issue2142 + + source, _ = read_data("missing_final_newline.py") + # read_data adds a trailing newline + source = source.rstrip() + expected, _ = read_data("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 " + 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)]) + self.assertEqual(result.exit_code, 0) + finally: + os.unlink(tmp_file) + actual = result.output + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 24e8dad575f2ae373e64b583f6ad103cf9193781 Mon Sep 17 00:00:00 2001 From: James <50501825+Gobot1234@users.noreply.github.com> Date: Mon, 22 Feb 2021 15:42:05 +0000 Subject: [PATCH 143/680] Fix for enum changes in 3.10 (#1999) --- 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 c1907d9b1f1..e09f08df7df 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -883,7 +883,7 @@ def format_file_in_place( dst_name = f"{src}\t{now} +0000" diff_contents = diff(src_contents, dst_contents, src_name, dst_name) - if write_back == write_back.COLOR_DIFF: + if write_back == WriteBack.COLOR_DIFF: diff_contents = color_diff(diff_contents) with lock or nullcontext(): From fe4a9d6bee21c471139e07fa27b464187477556c Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 22 Feb 2021 15:46:38 +0000 Subject: [PATCH 144/680] Fixup: update function name in docs to match source (#1997) --- docs/reference/reference_functions.rst | 2 +- src/black/__init__.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/reference/reference_functions.rst b/docs/reference/reference_functions.rst index a7184115c94..bffce7e9e32 100644 --- a/docs/reference/reference_functions.rst +++ b/docs/reference/reference_functions.rst @@ -171,7 +171,7 @@ Utilities .. autofunction:: black.re_compile_maybe_verbose -.. autofunction:: black.should_split_body_explode +.. autofunction:: black.should_split_line .. autofunction:: black.shutdown diff --git a/src/black/__init__.py b/src/black/__init__.py index e09f08df7df..07251d55fb5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1480,7 +1480,7 @@ class Line: comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict) bracket_tracker: BracketTracker = field(default_factory=BracketTracker) inside_brackets: bool = False - should_split: bool = False + should_split_rhs: bool = False magic_trailing_comma: Optional[Leaf] = None def append(self, leaf: Leaf, preformatted: bool = False) -> None: @@ -1792,7 +1792,7 @@ def clone(self) -> "Line": mode=self.mode, depth=self.depth, inside_brackets=self.inside_brackets, - should_split=self.should_split, + should_split_rhs=self.should_split_rhs, magic_trailing_comma=self.magic_trailing_comma, ) @@ -2712,7 +2712,7 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: transformers: List[Transformer] if ( not line.contains_uncollapsable_type_comments() - and not line.should_split + 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) @@ -4387,7 +4387,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: mode=line.mode, depth=line.depth + 1, inside_brackets=True, - should_split=line.should_split, + should_split_rhs=line.should_split_rhs, magic_trailing_comma=line.magic_trailing_comma, ) string_leaf = Leaf(token.STRING, string_value) @@ -5010,7 +5010,7 @@ def bracket_split_build_line( for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) if is_body and should_split_line(result, opening_bracket): - result.should_split = True + result.should_split_rhs = True return result From e1c86f987eca7e532f7d69f7ff4b9c70432fabbf Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 22 Feb 2021 15:49:38 +0000 Subject: [PATCH 145/680] Fuzzer testing: less strict special-case regex match passthrough for multi-line EOF exceptions (#1998) --- fuzz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzz.py b/fuzz.py index 6e2068690b5..a9ca8eff8b0 100644 --- a/fuzz.py +++ b/fuzz.py @@ -50,9 +50,9 @@ def test_idempotent_any_syntatically_valid_python( # TODO: remove this try-except block when issues are resolved. return except TokenError as e: - if ( + if ( # Special-case logic for backslashes followed by newlines or end-of-input e.args[0] == "EOF in multi-line statement" - and re.search(r"\r?\n\\\r?\n", src_contents) is not None + and re.search(r"\\($|\r?\n)", src_contents) is not None ): # This is a bug - if it's valid Python code, as above, Black should be # able to cope with it. See issue #1012. From b06cd15666e5d766347cda0434dc6c828a96c74a Mon Sep 17 00:00:00 2001 From: tpilewicz <31728184+tpilewicz@users.noreply.github.com> Date: Wed, 24 Feb 2021 12:56:56 +0100 Subject: [PATCH 146/680] Wrap arithmetic and binary arithmetic expressions in invisible parentheses (#2001) --- src/black/__init__.py | 13 ++++++++ tests/data/expression.diff | 30 +++++++++++++++++-- tests/data/expression.py | 24 +++++++++++++++ .../expression_skip_magic_trailing_comma.diff | 30 +++++++++++++++++-- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 07251d55fb5..6919468609c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2049,6 +2049,8 @@ 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) 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): @@ -5613,6 +5615,17 @@ def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: return wrapped +def first_child_is_arith(node: Node) -> bool: + """Whether first child is an arithmetic or a binary arithmetic expression""" + expr_types = { + 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 wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: """Wrap `child` in parentheses. diff --git a/tests/data/expression.diff b/tests/data/expression.diff index 684f92cd3b7..721a07d2141 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -166,7 +166,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,113 +173,180 @@ +@@ -137,118 +173,199 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -346,6 +346,9 @@ - return True -if ( - ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +-): +- return True +-aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz @@ -417,7 +420,28 @@ + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n - ): - return True ++): ++ return True ++( ++ aaaaaaaaaaaaaaaa ++ + aaaaaaaaaaaaaaaa ++ - aaaaaaaaaaaaaaaa ++ * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) ++ / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) ++) + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa +-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++) + bbbb >> bbbb * bbbb +-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++) last_call() # standalone comment at ENDMARKER diff --git a/tests/data/expression.py b/tests/data/expression.py index 8e63bdcdf9b..d13450cda68 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -245,6 +245,11 @@ async def f(): ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True +aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) +aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +bbbb >> bbbb * bbbb +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa last_call() # standalone comment at ENDMARKER @@ -602,5 +607,24 @@ async def f(): >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True +( + aaaaaaaaaaaaaaaa + + aaaaaaaaaaaaaaaa + - aaaaaaaaaaaaaaaa + * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) + / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) +) +aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) +bbbb >> bbbb * bbbb +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) last_call() # standalone comment at ENDMARKER diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff index 8a0225bceb5..4a8a95c7237 100644 --- a/tests/data/expression_skip_magic_trailing_comma.diff +++ b/tests/data/expression_skip_magic_trailing_comma.diff @@ -149,7 +149,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -137,113 +156,178 @@ +@@ -137,118 +156,197 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -327,6 +327,9 @@ - return True -if ( - ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n +-): +- return True +-aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz @@ -398,7 +401,28 @@ + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n - ): - return True ++): ++ return True ++( ++ aaaaaaaaaaaaaaaa ++ + aaaaaaaaaaaaaaaa ++ - aaaaaaaaaaaaaaaa ++ * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) ++ / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) ++) + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa +-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++) + bbbb >> bbbb * bbbb +-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++ ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++) last_call() # standalone comment at ENDMARKER From 858225d34dc49ca353f9e573dd82dd5845766115 Mon Sep 17 00:00:00 2001 From: Rishav Kundu Date: Sun, 28 Feb 2021 06:50:23 +0530 Subject: [PATCH 147/680] Strip redundant parentheses from assignment exprs (#1906) Fixes #1656 --- src/black/__init__.py | 10 +++++--- tests/data/pep_572.py | 4 +-- tests/data/pep_572_remove_parens.py | 38 +++++++++++++++++++++++++++++ tests/data/remove_parens.py | 2 -- tests/test_black.py | 9 +++++++ 5 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 tests/data/pep_572_remove_parens.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 6919468609c..a1d16d9bdeb 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5370,10 +5370,7 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: check_lpar = True if check_lpar: - if is_walrus_assignment(child): - pass - - elif child.type == syms.atom: + if child.type == syms.atom: if maybe_make_parens_invisible_in_atom(child, parent=node): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): @@ -5545,6 +5542,7 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: Returns whether the node should itself be wrapped in invisible parentheses. """ + if ( node.type != syms.atom or is_empty_tuple(node) @@ -5554,6 +5552,10 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: ): return False + if is_walrus_assignment(node): + if parent.type in [syms.annassign, syms.expr_stmt]: + return False + first = node.children[0] last = node.children[-1] if first.type == token.LPAR and last.type == token.RPAR: diff --git a/tests/data/pep_572.py b/tests/data/pep_572.py index 637b3bb38c6..c6867f26258 100644 --- a/tests/data/pep_572.py +++ b/tests/data/pep_572.py @@ -2,7 +2,7 @@ (a := a) if (match := pattern.search(data)) is None: pass -if (match := pattern.search(data)): +if match := pattern.search(data): pass [y := f(x), y ** 2, y ** 3] filtered_data = [y for x in data if (y := f(x)) is None] @@ -43,5 +43,5 @@ def foo(answer: (p := 42) = 5): while x := f(x): pass -while (x := f(x)): +while x := f(x): pass diff --git a/tests/data/pep_572_remove_parens.py b/tests/data/pep_572_remove_parens.py new file mode 100644 index 00000000000..04cc75bc36c --- /dev/null +++ b/tests/data/pep_572_remove_parens.py @@ -0,0 +1,38 @@ +if (foo := 0): + pass + +if (foo := 1): + pass + +if (y := 5 + 5): + pass + +y = (x := 0) + +y += (x := 0) + +(y := 5 + 5) + +test: int = (test2 := 2) + +a, b = (test := (1, 2)) + +# output +if foo := 0: + pass + +if foo := 1: + pass + +if y := 5 + 5: + pass + +y = (x := 0) + +y += (x := 0) + +(y := 5 + 5) + +test: int = (test2 := 2) + +a, b = (test := (1, 2)) diff --git a/tests/data/remove_parens.py b/tests/data/remove_parens.py index afc34010c30..abd5f71fcd0 100644 --- a/tests/data/remove_parens.py +++ b/tests/data/remove_parens.py @@ -54,7 +54,6 @@ def example7(): def example8(): return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) - # output x = 1 x = 1.2 @@ -141,4 +140,3 @@ def example7(): def example8(): return None - diff --git a/tests/test_black.py b/tests/test_black.py index 5d14ceda8f4..9c3cc64387b 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -263,6 +263,15 @@ def test_pep_572(self) -> None: 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) + def test_pep_572_version_detection(self) -> None: source, _ = read_data("pep_572") root = black.lib2to3_parse(source) From beecd6fd0a9103aa91b1019dcf8fc774b667ea6c Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 1 Mar 2021 16:07:36 -0600 Subject: [PATCH 148/680] Add --extend-exclude parameter (#2005) Look ma! I contribute to open source! Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- README.md | 30 +++++------- docs/change_log.md | 2 + docs/installation_and_usage.md | 7 ++- docs/pyproject_toml.md | 22 ++------- pyproject.toml | 13 +---- src/black/__init__.py | 78 ++++++++++++++++++++---------- tests/test_black.py | 86 ++++++++++++++++++++++++++++------ 7 files changed, 148 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 178f763c32d..5f8b52cb823 100644 --- a/README.md +++ b/README.md @@ -135,11 +135,17 @@ Options: hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu ild|buck-out|build|dist)/] + --extend-exclude TEXT 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 TEXT Like --exclude, but files and directories matching this regex will be excluded even when they are passed explicitly as arguments. + --stdin-filename TEXT The name of the file when passing it through stdin. Useful to make sure Black will respect --force-exclude option on some @@ -151,7 +157,7 @@ Options: -v, --verbose Also emit messages to stderr about files that were not changed or were ignored due to - --exclude=. + exclusion patterns. --version Show the version and exit. --config FILE Read configuration from FILE path. @@ -263,7 +269,7 @@ above. What seems like a bug might be intended behaviour. _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude` patterns for your project. +`--include` and `--exclude`/`--extend-exclude` patterns for your project. **Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is "No". _Black_ is all about sensible defaults. @@ -313,25 +319,10 @@ expressions by Black. Use `[ ]` to denote a significant space character. line-length = 88 target-version = ['py37'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories # in the root of the project. -^/( - ( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | foo.py # also separately exclude a file named foo.py 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) ''' ``` @@ -616,6 +607,7 @@ Multiple contributions by: - [Joseph Larson](mailto:larson.joseph@gmail.com) - [Josh Bode](mailto:joshbode@fastmail.com) - [Josh Holland](mailto:anowlcalledjosh@gmail.com) +- [Joshua Cannon](mailto:joshdcannon@gmail.com) - [José Padilla](mailto:jpadilla@webapplicate.com) - [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) - [kaiix](mailto:kvn.hou@gmail.com) diff --git a/docs/change_log.md b/docs/change_log.md index 066be76c06c..01c27553fca 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -28,6 +28,8 @@ - use lowercase hex strings (#1692) +- added `--extend-exclude` argument (#1571) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index ee45c934da8..bb554eb6744 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -95,6 +95,11 @@ Options: when they are passed explicitly as arguments. + --extend-exclude TEXT Like --exclude, but adds additional files + and directories on top of the excluded + ones. (useful if you simply want to add to + the default) + --stdin-filename TEXT The name of the file when passing it through stdin. Useful to make sure Black will respect --force-exclude option on some @@ -106,7 +111,7 @@ Options: -v, --verbose Also emit messages to stderr about files that were not changed or were ignored due to - --exclude=. + exclusion patterns. --version Show the version and exit. --config FILE Read configuration from FILE path. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 453f533bf96..9acc4c03d7c 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -4,7 +4,8 @@ _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude` patterns for your project. +`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your +project. **Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is "No". _Black_ is all about sensible defaults. @@ -54,25 +55,10 @@ expressions by Black. Use `[ ]` to denote a significant space character. line-length = 88 target-version = ['py37'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories # in the root of the project. -^/( - ( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | foo.py # also separately exclude a file named foo.py 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) ''' ``` diff --git a/pyproject.toml b/pyproject.toml index 9d4da0bf692..7f632f2839d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,19 +9,8 @@ line-length = 88 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - # The following are specific to Black, you probably don't want those. | blib2to3 | tests/data diff --git a/src/black/__init__.py b/src/black/__init__.py index a1d16d9bdeb..e21e2af5bd1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -461,6 +461,14 @@ def target_version_option_callback( ), show_default=True, ) +@click.option( + "--extend-exclude", + type=str, + 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)" + ), +) @click.option( "--force-exclude", type=str, @@ -493,7 +501,7 @@ def target_version_option_callback( is_flag=True, help=( "Also emit messages to stderr about files that were not changed or were ignored" - " due to --exclude=." + " due to exclusion patterns." ), ) @click.version_option(version=__version__) @@ -537,6 +545,7 @@ def main( verbose: bool, include: str, exclude: str, + extend_exclude: Optional[str], force_exclude: Optional[str], stdin_filename: Optional[str], src: Tuple[str, ...], @@ -570,6 +579,7 @@ def main( verbose=verbose, include=include, exclude=exclude, + extend_exclude=extend_exclude, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, @@ -602,6 +612,18 @@ def main( ctx.exit(report.return_code) +def test_regex( + ctx: click.Context, + regex_name: str, + regex: Optional[str], +) -> Optional[Pattern]: + try: + return re_compile_maybe_verbose(regex) if regex is not None else None + except re.error: + err(f"Invalid regular expression for {regex_name} given: {regex!r}") + ctx.exit(2) + + def get_sources( *, ctx: click.Context, @@ -610,28 +632,18 @@ def get_sources( verbose: bool, include: str, exclude: str, + extend_exclude: Optional[str], force_exclude: Optional[str], report: "Report", stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" - try: - include_regex = re_compile_maybe_verbose(include) - except re.error: - err(f"Invalid regular expression for include given: {include!r}") - ctx.exit(2) - try: - exclude_regex = re_compile_maybe_verbose(exclude) - except re.error: - err(f"Invalid regular expression for exclude given: {exclude!r}") - ctx.exit(2) - try: - force_exclude_regex = ( - re_compile_maybe_verbose(force_exclude) if force_exclude else None - ) - except re.error: - err(f"Invalid regular expression for force_exclude given: {force_exclude!r}") - ctx.exit(2) + + include_regex = test_regex(ctx, "include", include) + exclude_regex = test_regex(ctx, "exclude", exclude) + assert exclude_regex is not None + extend_exclude_regex = test_regex(ctx, "extend_exclude", extend_exclude) + force_exclude_regex = test_regex(ctx, "force_exclude", force_exclude) root = find_project_root(src) sources: Set[Path] = set() @@ -672,6 +684,7 @@ def get_sources( root, include_regex, exclude_regex, + extend_exclude_regex, force_exclude_regex, report, gitignore, @@ -6112,17 +6125,27 @@ def normalize_path_maybe_ignore( return normalized_path +def path_is_excluded( + normalized_path: str, + pattern: Optional[Pattern[str]], +) -> bool: + match = pattern.search(normalized_path) if pattern else None + return bool(match and match.group(0)) + + def gen_python_files( paths: Iterable[Path], root: Path, include: Optional[Pattern[str]], exclude: Pattern[str], + extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: "Report", gitignore: PathSpec, ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the - `exclude_regex` or `force_exclude` regexes, but are included by the `include` regex. + `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, + but are included by the `include` regex. Symbolic links pointing outside of the `root` directory are ignored. @@ -6139,20 +6162,22 @@ def gen_python_files( report.path_ignored(child, "matches the .gitignore file content") continue - # Then ignore with `--exclude` and `--force-exclude` options. + # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. normalized_path = "/" + normalized_path if child.is_dir(): normalized_path += "/" - exclude_match = exclude.search(normalized_path) if exclude else None - if exclude_match and exclude_match.group(0): + if path_is_excluded(normalized_path, exclude): report.path_ignored(child, "matches the --exclude regular expression") continue - force_exclude_match = ( - force_exclude.search(normalized_path) if force_exclude else None - ) - if force_exclude_match and force_exclude_match.group(0): + if path_is_excluded(normalized_path, extend_exclude): + report.path_ignored( + child, "matches the --extend-exclude regular expression" + ) + continue + + if path_is_excluded(normalized_path, force_exclude): report.path_ignored(child, "matches the --force-exclude regular expression") continue @@ -6162,6 +6187,7 @@ def gen_python_files( root, include, exclude, + extend_exclude, force_exclude, report, gitignore, diff --git a/tests/test_black.py b/tests/test_black.py index 9c3cc64387b..ba1869aa4c5 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1346,7 +1346,14 @@ def test_include_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1370,6 +1377,7 @@ def test_exclude_for_issue_1572(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1392,6 +1400,7 @@ def test_get_sources_with_stdin(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1415,6 +1424,7 @@ def test_get_sources_with_stdin_filename(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1442,6 +1452,35 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None: verbose=False, include=include, exclude=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=include, + exclude="", + extend_exclude=extend_exclude, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1467,6 +1506,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: verbose=False, include=include, exclude="", + extend_exclude=None, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, @@ -1551,7 +1591,14 @@ def test_gitignore_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1581,25 +1628,21 @@ def test_empty_include(self) -> None: empty, re.compile(black.DEFAULT_EXCLUDES), None, + None, report, gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) - def test_empty_exclude(self) -> None: + def test_extend_exclude(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/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), Path(path / "b/exclude/a.py"), - Path(path / "b/exclude/a.pyi"), - Path(path / "b/.definitely_exclude/a.py"), - Path(path / "b/.definitely_exclude/a.pyi"), + Path(path / "b/dont_exclude/a.py"), ] this_abs = THIS_DIR.resolve() sources.extend( @@ -1607,7 +1650,8 @@ def test_empty_exclude(self) -> None: path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - empty, + re.compile(r"\.pyi$"), + re.compile(r"\.definitely_exclude"), None, report, gitignore, @@ -1615,8 +1659,8 @@ def test_empty_exclude(self) -> None: ) self.assertEqual(sorted(expected), sorted(sources)) - def test_invalid_include_exclude(self) -> None: - for option in ["--include", "--exclude"]: + def test_invalid_cli_regex(self) -> None: + for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) def test_preserves_line_endings(self) -> None: @@ -1665,7 +1709,14 @@ def test_symlink_out_of_root_directory(self) -> None: try: list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, ) ) except ValueError as ve: @@ -1679,7 +1730,14 @@ def test_symlink_out_of_root_directory(self) -> None: with self.assertRaises(ValueError): list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, ) ) path.iterdir.assert_called() From cac18293d5a6bd6b34a953f9cb5413f9826e505f Mon Sep 17 00:00:00 2001 From: Austin Pray <71290498+austinpray-mixpanel@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:35:57 -0600 Subject: [PATCH 149/680] Adds --stdin-filename back to changelog (#2017) * Adds --stdin-filename back to changelog Looks like this went missing https://github.com/psf/black/commit/dea81b7ad5cfa04c3572771c34af823449d0a8f3#diff-729efdd61772b108539268bdbfd7472521bdc05a7cae6113f62ed2649a3ad9c7 * Update CHANGES.md Co-authored-by: Jelle Zijlstra * Update CHANGES.md Co-authored-by: Jelle Zijlstra Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 3fd7ad40496..90e5143fafe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,9 @@ - `--diff` correctly indicates when a file doesn't end in a newline (#1662) +- Added `--stdin-filename` argument to allow stdin to respect `--force-exclude` rules + (#1780) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub From e3c71c3a477a44e6d817d37825a59bc6ba6a9897 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Tue, 2 Mar 2021 19:21:50 -0600 Subject: [PATCH 150/680] Turn test_regex into a click callback (#2016) Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- src/black/__init__.py | 61 ++++++++++++++++++++----------------------- tests/test_black.py | 28 ++++++++++---------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index e21e2af5bd1..a8f4f89a6bb 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -363,6 +363,17 @@ def target_version_option_callback( return [TargetVersion[val.upper()] for val in v] +def validate_regex( + ctx: click.Context, + param: click.Parameter, + value: Optional[str], +) -> Optional[Pattern]: + 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") + + @click.command(context_settings=dict(help_option_names=["-h", "--help"])) @click.option("-c", "--code", type=str, help="Format the code passed in as a string.") @click.option( @@ -441,6 +452,7 @@ def target_version_option_callback( "--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" @@ -453,6 +465,7 @@ def target_version_option_callback( "--exclude", type=str, default=DEFAULT_EXCLUDES, + callback=validate_regex, help=( "A regular expression that matches files and directories that should be" " excluded on recursive searches. An empty value means no paths are excluded." @@ -464,6 +477,7 @@ def target_version_option_callback( @click.option( "--extend-exclude", type=str, + 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)" @@ -472,6 +486,7 @@ def target_version_option_callback( @click.option( "--force-exclude", 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." @@ -543,10 +558,10 @@ def main( experimental_string_processing: bool, quiet: bool, verbose: bool, - include: str, - exclude: str, - extend_exclude: Optional[str], - force_exclude: Optional[str], + include: Pattern, + exclude: Pattern, + extend_exclude: Optional[Pattern], + force_exclude: Optional[Pattern], stdin_filename: Optional[str], src: Tuple[str, ...], config: Optional[str], @@ -612,39 +627,21 @@ def main( ctx.exit(report.return_code) -def test_regex( - ctx: click.Context, - regex_name: str, - regex: Optional[str], -) -> Optional[Pattern]: - try: - return re_compile_maybe_verbose(regex) if regex is not None else None - except re.error: - err(f"Invalid regular expression for {regex_name} given: {regex!r}") - ctx.exit(2) - - def get_sources( *, ctx: click.Context, src: Tuple[str, ...], quiet: bool, verbose: bool, - include: str, - exclude: str, - extend_exclude: Optional[str], - force_exclude: Optional[str], + include: Pattern[str], + exclude: Pattern[str], + extend_exclude: Optional[Pattern[str]], + force_exclude: Optional[Pattern[str]], report: "Report", stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" - include_regex = test_regex(ctx, "include", include) - exclude_regex = test_regex(ctx, "exclude", exclude) - assert exclude_regex is not None - extend_exclude_regex = test_regex(ctx, "extend_exclude", extend_exclude) - force_exclude_regex = test_regex(ctx, "force_exclude", force_exclude) - root = find_project_root(src) sources: Set[Path] = set() path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) @@ -665,8 +662,8 @@ def get_sources( normalized_path = "/" + normalized_path # Hard-exclude any files that matches the `--force-exclude` regex. - if force_exclude_regex: - force_exclude_match = force_exclude_regex.search(normalized_path) + 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): @@ -682,10 +679,10 @@ def get_sources( gen_python_files( p.iterdir(), root, - include_regex, - exclude_regex, - extend_exclude_regex, - force_exclude_regex, + include, + exclude, + extend_exclude, + force_exclude, report, gitignore, ) diff --git a/tests/test_black.py b/tests/test_black.py index ba1869aa4c5..72e16a324a5 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1375,8 +1375,8 @@ def test_exclude_for_issue_1572(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), extend_exclude=None, force_exclude=None, report=report, @@ -1398,8 +1398,8 @@ def test_get_sources_with_stdin(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), extend_exclude=None, force_exclude=None, report=report, @@ -1422,8 +1422,8 @@ def test_get_sources_with_stdin_filename(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), extend_exclude=None, force_exclude=None, report=report, @@ -1450,8 +1450,8 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), extend_exclude=None, force_exclude=None, report=report, @@ -1478,9 +1478,9 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude="", - extend_exclude=extend_exclude, + include=re.compile(include), + exclude=re.compile(""), + extend_exclude=re.compile(extend_exclude), force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1504,10 +1504,10 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: src=(src,), quiet=True, verbose=False, - include=include, - exclude="", + include=re.compile(include), + exclude=re.compile(""), extend_exclude=None, - force_exclude=force_exclude, + force_exclude=re.compile(force_exclude), report=report, stdin_filename=stdin_filename, ) From e293473ea9c6a4408d4eb489d9d61fc87dbec639 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta Date: Thu, 4 Mar 2021 06:16:27 +0530 Subject: [PATCH 151/680] Add formatters-python for atom to editor_integration (#1834) --- docs/editor_integration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index aa3a1eeedec..bc7c2e7b7be 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -286,7 +286,8 @@ Sublime Text, Visual Studio Code and many more), you can use the ## Atom/Nuclide -Use [python-black](https://atom.io/packages/python-black). +Use [python-black](https://atom.io/packages/python-black) or +[formatters-python](https://atom.io/packages/formatters-python). ## Gradle (the build tool) From 24418a54501381e1cdbe0ab1de4e4ced7537c124 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Mar 2021 15:59:31 +0200 Subject: [PATCH 152/680] Black requires Python 3.6.2+ (#1668) --- README.md | 2 +- plugin/black.vim | 6 +++--- setup.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5f8b52cb823..411a8c8609d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ _Contents:_ **[Installation and usage](#installation-and-usage)** | ### Installation -_Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to +_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to run but you can reformat Python 2 code with it, too. #### Install from GitHub diff --git a/plugin/black.vim b/plugin/black.vim index c5f0313f4ac..39a31f14b52 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -103,9 +103,9 @@ def _get_virtualenv_site_packages(venv_path, pyver): return venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}' / 'site-packages' def _initialize_black_env(upgrade=False): - pyver = sys.version_info[:2] - if pyver < (3, 6): - print("Sorry, Black requires Python 3.6+ to run.") + pyver = sys.version_info[:3] + if pyver < (3, 6, 2): + print("Sorry, Black requires Python 3.6.2+ to run.") return False from pathlib import Path diff --git a/setup.py b/setup.py index 48160568265..efdf6933025 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys import os -assert sys.version_info >= (3, 6, 0), "black requires Python 3.6+" +assert sys.version_info >= (3, 6, 2), "black requires Python 3.6.2+" from pathlib import Path # noqa E402 CURRENT_DIR = Path(__file__).parent @@ -65,7 +65,7 @@ def get_long_description() -> str: packages=["blackd", "black", "blib2to3", "blib2to3.pgen2", "black_primer"], package_dir={"": "src"}, package_data={"blib2to3": ["*.txt"], "black": ["py.typed"]}, - python_requires=">=3.6", + python_requires=">=3.6.2", zip_safe=False, install_requires=[ "click>=7.1.2", From 12f98219bc83fa186cc97db7b614dccfb2a1db71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:43:54 -0800 Subject: [PATCH 153/680] Bump aiohttp from 3.7.3 to 3.7.4 (#2009) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.3 to 3.7.4. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.3...v3.7.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 279 +++++++++++++++++++++++++-------------------------- 1 file changed, 138 insertions(+), 141 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 9aa2d0e7ce8..9cdcba2a0ea 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,46 +16,46 @@ "default": { "aiohttp": { "hashes": [ - "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", - "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", - "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", - "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", - "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", - "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", - "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", - "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", - "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", - "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", - "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", - "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", - "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", - "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", - "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", - "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", - "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", - "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", - "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", - "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", - "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", - "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", - "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", - "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", - "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", - "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", - "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", - "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", - "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", - "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", - "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", - "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", - "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", - "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", - "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", - "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", - "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", + "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", + "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", + "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", + "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", + "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", + "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", + "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", + "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", + "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", + "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", + "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", + "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", + "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", + "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", + "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", + "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", + "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", + "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", + "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", + "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", + "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", + "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", + "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", + "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", + "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", + "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", + "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", + "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", + "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", + "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", + "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", + "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", + "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", + "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", + "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", + "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" ], "index": "pypi", - "version": "==3.7.3" + "version": "==3.7.4" }, "aiohttp-cors": { "hashes": [ @@ -328,46 +328,46 @@ "develop": { "aiohttp": { "hashes": [ - "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", - "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", - "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", - "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", - "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", - "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", - "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", - "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", - "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", - "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", - "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", - "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", - "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", - "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", - "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", - "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", - "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", - "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", - "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", - "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", - "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", - "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", - "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", - "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", - "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", - "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", - "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", - "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", - "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", - "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", - "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", - "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", - "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", - "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", - "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", - "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", - "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", + "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", + "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", + "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", + "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", + "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", + "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", + "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", + "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", + "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", + "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", + "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", + "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", + "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", + "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", + "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", + "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", + "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", + "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", + "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", + "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", + "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", + "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", + "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", + "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", + "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", + "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", + "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", + "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", + "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", + "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", + "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", + "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", + "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", + "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", + "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", + "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" ], "index": "pypi", - "version": "==3.7.3" + "version": "==3.7.4" }, "aiohttp-cors": { "hashes": [ @@ -436,45 +436,45 @@ }, "cffi": { "hashes": [ - "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", - "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", - "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", - "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", - "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", - "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", - "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", - "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", - "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", - "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", - "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", - "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", - "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", - "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", - "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", - "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", - "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e", - "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", - "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", - "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", - "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", - "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", - "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", - "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", - "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", - "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", - "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", - "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", - "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", - "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", - "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", - "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", - "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", - "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", - "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", - "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", - "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" - ], - "version": "==1.14.4" + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" }, "cfgv": { "hashes": [ @@ -569,23 +569,20 @@ }, "cryptography": { "hashes": [ - "sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72", - "sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff", - "sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c", - "sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3", - "sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed", - "sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed", - "sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433", - "sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e", - "sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44", - "sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed", - "sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042", - "sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b", - "sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f", - "sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da" - ], - "index": "pypi", - "version": "==3.3.2" + "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b", + "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336", + "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87", + "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7", + "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799", + "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b", + "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df", + "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0", + "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3", + "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724", + "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2", + "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964" + ], + "version": "==3.4.6" }, "distlib": { "hashes": [ @@ -627,10 +624,10 @@ }, "identify": { "hashes": [ - "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66", - "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4" + "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc", + "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3" ], - "version": "==1.5.13" + "version": "==1.5.14" }, "idna": { "hashes": [ @@ -862,10 +859,10 @@ }, "pygments": { "hashes": [ - "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", - "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" + "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", + "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], - "version": "==2.7.4" + "version": "==2.8.0" }, "pyparsing": { "hashes": [ @@ -1081,10 +1078,10 @@ }, "tqdm": { "hashes": [ - "sha256:2874fa525c051177583ec59c0fb4583e91f28ccd3f217ffad2acdb32d2c789ac", - "sha256:ab9b659241d82b8b51b2269ee243ec95286046bf06015c4e15a947cc15914211" + "sha256:2c44efa73b8914dba7807aefd09653ac63c22b5b4ea34f7a80973f418f1a3089", + "sha256:c23ac707e8e8aabb825e4d91f8e17247f9cc14b0d64dd9e97be0781e9e525bba" ], - "version": "==4.56.1" + "version": "==4.58.0" }, "twine": { "hashes": [ From b3ceb293d9e69295a190fed93517cbe1b7372154 Mon Sep 17 00:00:00 2001 From: Hadi Alqattan Date: Fri, 5 Mar 2021 22:58:00 +0300 Subject: [PATCH 154/680] Remove unused import statements using Pycln. (#2021) * remove unused imports using Pycln. * reverse comma style. --- src/blib2to3/pgen2/driver.py | 3 --- src/blib2to3/pgen2/parse.py | 2 -- src/blib2to3/pgen2/pgen.py | 1 - src/blib2to3/pytree.py | 1 - 4 files changed, 7 deletions(-) diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 81940f78f0f..af1dc6b8aeb 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -16,7 +16,6 @@ __all__ = ["Driver", "load_grammar"] # Python imports -import codecs import io import os import logging @@ -24,7 +23,6 @@ import sys from typing import ( Any, - Callable, IO, Iterable, List, @@ -32,7 +30,6 @@ Text, Tuple, Union, - Sequence, ) # Pgen imports diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 8c374d35b42..47c8f02b4f5 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -15,8 +15,6 @@ from typing import ( Optional, Text, - Sequence, - Any, Union, Tuple, Dict, diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index a685145933c..564ebbd1184 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -8,7 +8,6 @@ Any, Dict, IO, - Iterable, Iterator, List, Optional, diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 6dba3c7bb15..0c074f6a4ac 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -25,7 +25,6 @@ Union, Set, Iterable, - Sequence, ) from blib2to3.pgen2.grammar import Grammar From 1f7e73506c8b8c37d72becad5c9d8f1ab61644e1 Mon Sep 17 00:00:00 2001 From: John Meow Date: Sat, 6 Mar 2021 07:22:19 +0300 Subject: [PATCH 155/680] Add ALE (#1753) --- docs/editor_integration.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index bc7c2e7b7be..ae24c768882 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -111,6 +111,8 @@ $ black --help ## Vim +### Official plugin + Commands and shortcuts: - `:Black` to format the entire file (ranges not supported); @@ -233,6 +235,17 @@ If you later want to update _Black_, you should do it like this: $ pip install -U black --no-binary regex,typed-ast ``` +### With ALE + +1. Install [`ale`](https://github.com/dense-analysis/ale) +2. Install `black` +3. Add this to your vimrc: + + ```vim + let g:ale_fixers = {} + let g:ale_fixers.python = ['black'] + ``` + ## Gedit gedit is the default text editor of the GNOME, Unix like Operating Systems. Open gedit From b332cfa655dedfeb74974abdac1fb7b55d7e62ea Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 6 Mar 2021 15:02:25 -0500 Subject: [PATCH 156/680] Add missing changelog entry for fmt: skip (#2025) --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 90e5143fafe..e6dcc1b967d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ - Added `--stdin-filename` argument to allow stdin to respect `--force-exclude` rules (#1780) +- Lines ending with `fmt: skip` will now be not formatted (#1800) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub From 5446a92f0161e398de765bf9532d8c76c5652333 Mon Sep 17 00:00:00 2001 From: Konstantin Alekseev Date: Mon, 8 Mar 2021 03:13:25 +0300 Subject: [PATCH 157/680] Use vim autoload script (#1157) --- autoload/black.vim | 172 +++++++++++++++++++++++++++++++++++++++++++++ plugin/black.vim | 172 ++------------------------------------------- 2 files changed, 179 insertions(+), 165 deletions(-) create mode 100644 autoload/black.vim diff --git a/autoload/black.vim b/autoload/black.vim new file mode 100644 index 00000000000..f0357b07123 --- /dev/null +++ b/autoload/black.vim @@ -0,0 +1,172 @@ +python3 << EndPython3 +import collections +import os +import sys +import vim +from distutils.util import strtobool + + +class Flag(collections.namedtuple("FlagBase", "name, cast")): + @property + def var_name(self): + return self.name.replace("-", "_") + + @property + def vim_rc_name(self): + name = self.var_name + if name == "line_length": + name = name.replace("_", "") + return "g:black_" + name + + +FLAGS = [ + Flag(name="line_length", cast=int), + Flag(name="fast", cast=strtobool), + Flag(name="string_normalization", cast=strtobool), + Flag(name="quiet", cast=strtobool), +] + + +def _get_python_binary(exec_prefix): + try: + default = vim.eval("g:pymode_python").strip() + except vim.error: + default = "" + if default and os.path.exists(default): + return default + if sys.platform[:3] == "win": + return exec_prefix / 'python.exe' + return exec_prefix / 'bin' / 'python3' + +def _get_pip(venv_path): + if sys.platform[:3] == "win": + return venv_path / 'Scripts' / 'pip.exe' + return venv_path / 'bin' / 'pip' + +def _get_virtualenv_site_packages(venv_path, pyver): + if sys.platform[:3] == "win": + return venv_path / 'Lib' / 'site-packages' + return venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}' / 'site-packages' + +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.") + return False + + from pathlib import Path + import subprocess + import venv + virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser() + virtualenv_site_packages = str(_get_virtualenv_site_packages(virtualenv_path, pyver)) + first_install = False + if not virtualenv_path.is_dir(): + print('Please wait, one time setup for Black.') + _executable = sys.executable + _base_executable = getattr(sys, "_base_executable", _executable) + try: + executable = str(_get_python_binary(Path(sys.exec_prefix))) + sys.executable = executable + sys._base_executable = executable + print(f'Creating a virtualenv in {virtualenv_path}...') + print('(this path can be customized in .vimrc by setting g:black_virtualenv)') + venv.create(virtualenv_path, with_pip=True) + except Exception: + print('Encountered exception while creating virtualenv (see traceback below).') + print(f'Removing {virtualenv_path}...') + import shutil + shutil.rmtree(virtualenv_path) + raise + finally: + sys.executable = _executable + sys._base_executable = _base_executable + first_install = True + if first_install: + print('Installing Black with pip...') + if upgrade: + print('Upgrading Black with pip...') + if first_install or upgrade: + subprocess.run([str(_get_pip(virtualenv_path)), 'install', '-U', 'black'], stdout=subprocess.PIPE) + print('DONE! You are all set, thanks for waiting ✨ 🍰 ✨') + if first_install: + print('Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and restart Vim.\n') + if virtualenv_site_packages not in sys.path: + sys.path.insert(0, virtualenv_site_packages) + return True + +if _initialize_black_env(): + import black + import time + +def Black(): + start = time.time() + configs = get_configs() + mode = black.FileMode( + line_length=configs["line_length"], + string_normalization=configs["string_normalization"], + is_pyi=vim.current.buffer.name.endswith('.pyi'), + ) + quiet = configs["quiet"] + + buffer_str = '\n'.join(vim.current.buffer) + '\n' + try: + new_buffer_str = black.format_file_contents( + buffer_str, + fast=configs["fast"], + mode=mode, + ) + except black.NothingChanged: + if not quiet: + print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)') + except Exception as exc: + print(exc) + else: + current_buffer = vim.current.window.buffer + cursors = [] + for i, tabpage in enumerate(vim.tabpages): + if tabpage.valid: + for j, window in enumerate(tabpage.windows): + if window.valid and window.buffer == current_buffer: + cursors.append((i, j, window.cursor)) + vim.current.buffer[:] = new_buffer_str.split('\n')[:-1] + for i, j, cursor in cursors: + window = vim.tabpages[i].windows[j] + try: + window.cursor = cursor + except vim.error: + window.cursor = (len(window.buffer), 0) + if not quiet: + print(f'Reformatted in {time.time() - start:.4f}s.') + +def get_configs(): + path_pyproject_toml = black.find_pyproject_toml(vim.eval("fnamemodify(getcwd(), ':t')")) + if path_pyproject_toml: + toml_config = black.parse_pyproject_toml(path_pyproject_toml) + else: + toml_config = {} + + return { + flag.var_name: flag.cast(toml_config.get(flag.name, vim.eval(flag.vim_rc_name))) + for flag in FLAGS + } + + +def BlackUpgrade(): + _initialize_black_env(upgrade=True) + +def BlackVersion(): + print(f'Black, version {black.__version__} on Python {sys.version}.') + +EndPython3 + +function black#Black() + :py3 Black() +endfunction + +function black#BlackUpgrade() + :py3 BlackUpgrade() +endfunction + +function black#BlackVersion() + :py3 BlackVersion() +endfunction diff --git a/plugin/black.vim b/plugin/black.vim index 39a31f14b52..b5edb2a6ade 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -2,7 +2,7 @@ " Author: Łukasz Langa " Created: Mon Mar 26 23:27:53 2018 -0700 " Requires: Vim Ver7.0+ -" Version: 1.1 +" Version: 1.2 " " Documentation: " This plugin formats Python files. @@ -12,6 +12,8 @@ " - initial version " 1.1: " - restore cursor/window position after formatting +" 1.2: +" - use autoload script if v:version < 700 || !has('python3') func! __BLACK_MISSING() @@ -24,7 +26,7 @@ if v:version < 700 || !has('python3') endif if exists("g:load_black") - finish + finish endif let g:load_black = "py1.0" @@ -52,167 +54,7 @@ if !exists("g:black_quiet") let g:black_quiet = 0 endif -python3 << EndPython3 -import collections -import os -import sys -import vim -from distutils.util import strtobool - -class Flag(collections.namedtuple("FlagBase", "name, cast")): - @property - def var_name(self): - return self.name.replace("-", "_") - - @property - def vim_rc_name(self): - name = self.var_name - if name == "line_length": - name = name.replace("_", "") - return "g:black_" + name - - -FLAGS = [ - Flag(name="line_length", cast=int), - Flag(name="fast", cast=strtobool), - Flag(name="string_normalization", cast=strtobool), - Flag(name="quiet", cast=strtobool), -] - - -def _get_python_binary(exec_prefix): - try: - default = vim.eval("g:pymode_python").strip() - except vim.error: - default = "" - if default and os.path.exists(default): - return default - if sys.platform[:3] == "win": - return exec_prefix / 'python.exe' - return exec_prefix / 'bin' / 'python3' - -def _get_pip(venv_path): - if sys.platform[:3] == "win": - return venv_path / 'Scripts' / 'pip.exe' - return venv_path / 'bin' / 'pip' - -def _get_virtualenv_site_packages(venv_path, pyver): - if sys.platform[:3] == "win": - return venv_path / 'Lib' / 'site-packages' - return venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}' / 'site-packages' - -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.") - return False - - from pathlib import Path - import subprocess - import venv - virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser() - virtualenv_site_packages = str(_get_virtualenv_site_packages(virtualenv_path, pyver)) - first_install = False - if not virtualenv_path.is_dir(): - print('Please wait, one time setup for Black.') - _executable = sys.executable - _base_executable = getattr(sys, "_base_executable", _executable) - try: - executable = str(_get_python_binary(Path(sys.exec_prefix))) - sys.executable = executable - sys._base_executable = executable - print(f'Creating a virtualenv in {virtualenv_path}...') - print('(this path can be customized in .vimrc by setting g:black_virtualenv)') - venv.create(virtualenv_path, with_pip=True) - except Exception: - print('Encountered exception while creating virtualenv (see traceback below).') - print(f'Removing {virtualenv_path}...') - import shutil - shutil.rmtree(virtualenv_path) - raise - finally: - sys.executable = _executable - sys._base_executable = _base_executable - first_install = True - if first_install: - print('Installing Black with pip...') - if upgrade: - print('Upgrading Black with pip...') - if first_install or upgrade: - subprocess.run([str(_get_pip(virtualenv_path)), 'install', '-U', 'black'], stdout=subprocess.PIPE) - print('DONE! You are all set, thanks for waiting ✨ 🍰 ✨') - if first_install: - print('Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and restart Vim.\n') - if virtualenv_site_packages not in sys.path: - sys.path.insert(0, virtualenv_site_packages) - return True - -if _initialize_black_env(): - import black - import time - -def Black(): - start = time.time() - configs = get_configs() - mode = black.FileMode( - line_length=configs["line_length"], - string_normalization=configs["string_normalization"], - is_pyi=vim.current.buffer.name.endswith('.pyi'), - ) - quiet = configs["quiet"] - - buffer_str = '\n'.join(vim.current.buffer) + '\n' - try: - new_buffer_str = black.format_file_contents( - buffer_str, - fast=configs["fast"], - mode=mode, - ) - except black.NothingChanged: - if not quiet: - print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)') - except Exception as exc: - print(exc) - else: - current_buffer = vim.current.window.buffer - cursors = [] - for i, tabpage in enumerate(vim.tabpages): - if tabpage.valid: - for j, window in enumerate(tabpage.windows): - if window.valid and window.buffer == current_buffer: - cursors.append((i, j, window.cursor)) - vim.current.buffer[:] = new_buffer_str.split('\n')[:-1] - for i, j, cursor in cursors: - window = vim.tabpages[i].windows[j] - try: - window.cursor = cursor - except vim.error: - window.cursor = (len(window.buffer), 0) - if not quiet: - print(f'Reformatted in {time.time() - start:.4f}s.') - -def get_configs(): - path_pyproject_toml = black.find_pyproject_toml(vim.eval("fnamemodify(getcwd(), ':t')")) - if path_pyproject_toml: - toml_config = black.parse_pyproject_toml(path_pyproject_toml) - else: - toml_config = {} - - return { - flag.var_name: flag.cast(toml_config.get(flag.name, vim.eval(flag.vim_rc_name))) - for flag in FLAGS - } - - -def BlackUpgrade(): - _initialize_black_env(upgrade=True) - -def BlackVersion(): - print(f'Black, version {black.__version__} on Python {sys.version}.') - -EndPython3 - -command! Black :py3 Black() -command! BlackUpgrade :py3 BlackUpgrade() -command! BlackVersion :py3 BlackVersion() +command! Black :call black#Black() +command! BlackUpgrade :call black#BlackUpgrade() +command! BlackVersion :call black#BlackVersion() From d62d677ca2135714d6159f6b4839983fb0a557c8 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Thu, 18 Mar 2021 15:14:15 +0000 Subject: [PATCH 158/680] Recommend B950 + 88 char limit instead of 80 (#2050) [The section about line length][1] was contradictory. On one side, it said: > Black will try to respect that [line length limit]. However, sometimes it won't be able to without breaking other rules. In those rare cases, auto-formatted code will exceed your allotted limit. So black doesn't guarantee that your code is formatted at 88 chars, even when configured with `--line-length=88` (default). Black uses this limit as a "hint" more than a "rule". OTOH, it also said: > If you're using Flake8, you can bump max-line-length to 88 and forget about it. Alternatively, use Bugbear's B950 warning instead of E501 and keep the max line length at 80 which you are probably already using. But that's not true. You can't "forget about it" because Black sometimes won't respect the limit. Both E501 at 88 and B950 at 80 behave the same: linter error at 89+ length. So, if Black happens to decide that a line of code is better at 90 characters that some other fancy style, you land on a unlucky situation where both tools will fight. So, AFAICS, the best way to align flake8 and black is to: 1. Use flake8-bugbear 2. Enable B950 3. Disable E501 4. Set `max-line-length = 88` This way, we also tell flake8 that 88 limit is a "hint" and not a "rule". The real rule will be 88 + 10%. If black decides that a line fits better in 97 characters than in 88 + some formatting, _that_ probably means your code has a real problem. To avoid further confusion, I change the official recommendation here. [1]: https://github.com/PyCQA/flake8-bugbear/tree/e82bb8d8b855adbf1f6f9757fb1527e93039e0d9#opinionated-warnings --- docs/the_black_code_style.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index a4e55c1744c..1cc591b8031 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -189,22 +189,22 @@ 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 forget about it. -Alternatively, use [Bugbear](https://github.com/PyCQA/flake8-bugbear)'s B950 warning -instead of E501 and keep the max line length at 80 which you are probably already using. -You'd do it like this: +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 = 80 +max-line-length = 88 ... select = C,E,F,W,B,B950 extend-ignore = E203, E501 ``` -You'll find _Black_'s own .flake8 config file is configured like this. Explanation of -why E203 is disabled can be found further in this documentation. And if you're curious -about the reasoning behind B950, +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". From 0be7f96d9ce764db1b2b6e176f6e74a3cdece665 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 20 Mar 2021 15:15:55 -0400 Subject: [PATCH 159/680] Fix indentation in docs/editor_integration.md (#2056) Numbered list entries' bodies need to be indented or else the list won't render correctly. --- docs/editor_integration.md | 68 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/editor_integration.md b/docs/editor_integration.md index ae24c768882..f2d21f21113 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -12,38 +12,38 @@ Options include the following: 1. Install `black`. -```console -$ pip install black -``` + ```console + $ pip install black + ``` 2. Locate your `black` installation folder. -On macOS / Linux / BSD: + On macOS / Linux / BSD: -```console -$ which black -/usr/local/bin/black # possible location -``` + ```console + $ which black + /usr/local/bin/black # possible location + ``` -On Windows: + On Windows: -```console -$ where black -%LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location -``` + ```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`. + 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`. 3. Open External tools in PyCharm/IntelliJ IDEA -On macOS: + On macOS: -`PyCharm -> Preferences -> Tools -> External Tools` + `PyCharm -> Preferences -> Tools -> External Tools` -On Windows / Linux / BSD: + On Windows / Linux / BSD: -`File -> Settings -> Tools -> External Tools` + `File -> Settings -> Tools -> External Tools` 4. Click the + icon to add a new external tool with the following values: @@ -83,28 +83,28 @@ Wing supports black via the OS Commands tool, as explained in the Wing documenta 1. Install `black`. -```console -$ pip install black -``` + ```console + $ pip install black + ``` 2. Make sure it runs from the command line, e.g. -```console -$ black --help -``` + ```console + $ black --help + ``` 3. 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 + - 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 4. Select a file in the editor and press **F1** , or whatever key binding you selected in step 3, to reformat the file. From 5d33f20a2a2c85cfb521ae9c5f9254bfe9fc2fd9 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 21 Mar 2021 13:05:24 -0700 Subject: [PATCH 160/680] Add a GitHub CHANGELOG/News Check (#2057) - Run grep to see commit has a line mentioning it to CHANGES.md - Also add a label to disable this being required for PRs that don't need a change entry --- .github/workflows/changelog.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/changelog.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000000..434950d24f9 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,21 @@ +name: changelog + +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled, reopened] + +jobs: + build: + name: Changelog Entry Check + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Grep CHANGES.md for PR number + if: contains(github.event.pull_request.labels.*.name, 'skip news') != true + run: | + grep -P "PR #${{ github.event.pull_request.number }}[^0-9]" CHANGES.md || \ + (echo "Please add 'PR #${{ github.event.pull_request.number }}' change line to CHANGES.md" && \ + exit 1) From 580b4fe8bc8d859c228f2eea1c8335bb3e911b12 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 24 Mar 2021 19:38:07 -0400 Subject: [PATCH 161/680] Add entry for `--extend-exclude` in the right place (#2061) The PR author added the changelog entry for their `extend-exclude` addition in `docs/change_log.md` which is understandable but incorrect as it will be overwritten since it's autogenerated from the readme. --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e6dcc1b967d..61fc35f43a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,8 @@ - use lowercase hex strings (#1692) +- added `--extend-exclude` argument (PR #2005) + - speed up caching by avoiding pathlib (#1950) - `--diff` correctly indicates when a file doesn't end in a newline (#1662) From 4218ae18c06df82904b9c9eac166510ac0ebee7c Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 26 Mar 2021 07:21:18 -0700 Subject: [PATCH 162/680] Add `--skip-magic-trailing-comma` to CHANGES.md (#2064) --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 61fc35f43a4..7da7be7b842 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,9 @@ - `Black` no longer adds an incorrect space after a parenthesized assignment expression in if/while statements (#1655) +- Added `--skip-magic-trailing-comma` / `-C` to avoid using trailing commas as a reason + to split lines (#1824) + - fixed a crash when PWD=/ on POSIX (#1631) - fixed "I/O operation on closed file" when using --diff (#1664) From dc8b0a43837a6280e0d7d7d4e71a5282083c6b01 Mon Sep 17 00:00:00 2001 From: Mark Bell Date: Mon, 29 Mar 2021 00:01:37 +0100 Subject: [PATCH 163/680] GREP for PR reference accepts references that are split over a line (#2072) Fixes #2070 --- .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 434950d24f9..58a8c092edb 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -16,6 +16,6 @@ jobs: - name: Grep CHANGES.md for PR number if: contains(github.event.pull_request.labels.*.name, 'skip news') != true run: | - grep -P "PR #${{ github.event.pull_request.number }}[^0-9]" CHANGES.md || \ + grep -Pz "PR( |\n\s*)#${{ github.event.pull_request.number }}[^0-9]" CHANGES.md || \ (echo "Please add 'PR #${{ github.event.pull_request.number }}' change line to CHANGES.md" && \ exit 1) From c702588daa9f75855119d0656661ac17934efe35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 18:59:57 -0700 Subject: [PATCH 164/680] Bump pygments from 2.6.1 to 2.7.4 in /docs (#2076) Bumps [pygments](https://github.com/pygments/pygments) from 2.6.1 to 2.7.4. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.6.1...2.7.4) 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 4cad9bc205b..fcb6809cadc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,3 @@ recommonmark==0.6.0 Sphinx==3.2.1 -Pygments==2.6.1 \ No newline at end of file +Pygments==2.7.4 \ No newline at end of file From ed9d58b7410f24446cedebd0f767b71b154f5f5b Mon Sep 17 00:00:00 2001 From: KotlinIsland Date: Tue, 16 Mar 2021 19:31:18 +1000 Subject: [PATCH 165/680] don't require typed-ast --- CHANGES.md | 5 +++++ README.md | 3 ++- setup.py | 3 ++- src/black/__init__.py | 22 ++++++++++++++++++++-- tox.ini | 2 +- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7da7be7b842..97a3be33c93 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,11 @@ - Lines ending with `fmt: skip` will now be not formatted (#1800) +- PR #2053: Black no longer relies on typed-ast for Python 3.8 and higher + +- PR #2053: Python 2 support is now optional, install with + `python3 -m pip install black[python2]` to maintain support. + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/README.md b/README.md index 411a8c8609d..0be356e3b84 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,8 @@ _Contents:_ **[Installation and usage](#installation-and-usage)** | ### Installation _Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to -run but you can reformat Python 2 code with it, too. +run. If you want to format Python 2 code as well, install with +`pip install black[python2]`. #### Install from GitHub diff --git a/setup.py b/setup.py index efdf6933025..856c7fadb0c 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,7 @@ def get_long_description() -> str: "click>=7.1.2", "appdirs", "toml>=0.10.1", - "typed-ast>=1.4.2", + "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", "pathspec>=0.6, <1", "dataclasses>=0.6; python_version < '3.7'", @@ -81,6 +81,7 @@ def get_long_description() -> str: extras_require={ "d": ["aiohttp>=3.3.2", "aiohttp-cors"], "colorama": ["colorama>=0.4.3"], + "python2": ["typed-ast>=1.4.2"], }, test_suite="tests.test_black", classifiers=[ diff --git a/src/black/__init__.py b/src/black/__init__.py index a8f4f89a6bb..52a57695aef 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -48,7 +48,20 @@ from dataclasses import dataclass, field, replace import click import toml -from typed_ast import ast3, ast27 + +try: + from typed_ast import ast3, ast27 +except ImportError: + if sys.version_info < (3, 8): + print( + "The typed_ast package is not installed.\n" + "You can install it with `python3 -m pip install typed-ast`.", + file=sys.stderr, + ) + sys.exit(1) + else: + ast3 = ast27 = ast + from pathspec import PathSpec # lib2to3 fork @@ -6336,7 +6349,12 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: return ast3.parse(src, filename, feature_version=feature_version) except SyntaxError: continue - + if ast27.__name__ == "ast": + raise SyntaxError( + "The requested source code has invalid Python 3 syntax.\n" + "If you are trying to format Python 2 files please reinstall Black" + " with the 'python2' extra: `python3 -m pip install black[python2]`." + ) return ast27.parse(src) diff --git a/tox.ini b/tox.ini index 500a2cad579..9bb809abe41 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ skip_install = True deps = -r{toxinidir}/test_requirements.txt commands = - pip install -e .[d] + pip install -e .[d,python2] coverage erase coverage run -m pytest tests coverage report From 9451c57d1c60d70ddd55e21b44382ca96637398e Mon Sep 17 00:00:00 2001 From: Harish Rajagopal Date: Thu, 1 Apr 2021 18:39:18 +0200 Subject: [PATCH 166/680] Support for top-level user configuration (#1899) * Added support for top-level user configuration At the user level, a TOML config can be specified in the following locations: * Windows: ~\.black * Unix-like: $XDG_CONFIG_HOME/black (~/.config/black fallback) Instead of changing env vars for the entire black-primer process, they are now changed only for the black subprocess, using a tmpdir. --- README.md | 14 ++++++++++++ docs/pyproject_toml.md | 14 ++++++++++++ src/black/__init__.py | 22 ++++++++++++++++++- src/black_primer/lib.py | 47 ++++++++++++++++++++++++----------------- tests/test_black.py | 36 +++++++++++++++++++++++++++++-- 5 files changed, 111 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0be356e3b84..b19e6e5e098 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,20 @@ parent directories. It stops looking when it finds the file, or a `.git` directo If you're formatting standard input, _Black_ will look for configuration starting from the current working directory. +You can use a "global" configuration, stored in a specific location in your home +directory. This will be used as a fallback configuration, that is, it will be used if +and only if _Black_ doesn't find any configuration as mentioned above. Depending on your +operating system, this configuration file should be stored as: + +- Windows: `~\.black` +- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the + `XDG_CONFIG_HOME` environment variable is not set) + +Note that these are paths to the TOML file itself (meaning that they shouldn't be named +as `pyproject.toml`), not directories where you store the configuration. Here, `~` +refers to the path to your home directory. On Windows, this will be something like +`C:\\Users\UserName`. + 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. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 9acc4c03d7c..77c12b76be9 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -28,6 +28,20 @@ parent directories. It stops looking when it finds the file, or a `.git` directo If you're formatting standard input, _Black_ will look for configuration starting from the current working directory. +You can use a "global" configuration, stored in a specific location in your home +directory. This will be used as a fallback configuration, that is, it will be used if +and only if _Black_ doesn't find any configuration as mentioned above. Depending on your +operating system, this configuration file should be stored as: + +- Windows: `~\.black` +- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the + `XDG_CONFIG_HOME` environment variable is not set) + +Note that these are paths to the TOML file itself (meaning that they shouldn't be named +as `pyproject.toml`), not directories where you store the configuration. Here, `~` +refers to the path to your home directory. On Windows, this will be something like +`C:\\Users\UserName`. + 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. diff --git a/src/black/__init__.py b/src/black/__init__.py index 52a57695aef..e85ffb37f88 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -306,7 +306,11 @@ def find_pyproject_toml(path_search_start: Iterable[str]) -> Optional[str]: """Find the absolute filepath to a pyproject.toml if it exists""" path_project_root = find_project_root(path_search_start) path_pyproject_toml = path_project_root / "pyproject.toml" - return str(path_pyproject_toml) if path_pyproject_toml.is_file() else None + if path_pyproject_toml.is_file(): + return str(path_pyproject_toml) + + path_user_pyproject_toml = find_user_pyproject_toml() + return str(path_user_pyproject_toml) if path_user_pyproject_toml.is_file() else None def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: @@ -6248,6 +6252,22 @@ def find_project_root(srcs: Iterable[str]) -> Path: return directory +@lru_cache() +def find_user_pyproject_toml() -> Path: + r"""Return the path to the top-level user configuration for black. + + This looks for ~\.black on Windows and ~/.config/black on Linux and other + Unix systems. + """ + if sys.platform == "win32": + # Windows + user_config_path = Path.home() / ".black" + else: + config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config") + user_config_path = Path(config_root).expanduser() / "black" + return user_config_path.resolve() + + @dataclass class Report: """Provides a reformatting counter. Can be rendered with `str(report)`.""" diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 39ae93ba516..7999f6a3e36 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -13,6 +13,7 @@ from shutil import rmtree, which from subprocess import CalledProcessError from sys import version_info +from tempfile import TemporaryDirectory from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence, Tuple from urllib.parse import urlparse @@ -121,28 +122,36 @@ async def black_run( cmd.extend(*project_config["cli_arguments"]) cmd.extend(["--check", "--diff", "."]) - try: - _stdout, _stderr = await _gen_check_output(cmd, cwd=repo_path) - 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"]: + with TemporaryDirectory() as tmp_path: + # Prevent reading top-level user configs by manipulating envionment variables + env = { + **os.environ, + "XDG_CONFIG_HOME": tmp_path, # Unix-like + "USERPROFILE": tmp_path, # Windows (changes `Path.home()` output) + } + + try: + _stdout, _stderr = await _gen_check_output(cmd, cwd=repo_path, env=env) + 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 - else: - results.stats["success"] += 1 - return - elif cpe.returncode > 1: - results.stats["failed"] += 1 - results.failed_projects[repo_path.name] = cpe - return + return - LOG.error(f"Unknown error with {repo_path}") - raise + 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"]: diff --git a/tests/test_black.py b/tests/test_black.py index 72e16a324a5..c603233efc4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -296,6 +296,7 @@ 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( @@ -303,7 +304,9 @@ def test_expression_diff(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, ["--diff", str(tmp_file)]) + result = BlackRunner().invoke( + black.main, ["--diff", str(tmp_file), f"--config={config}"] + ) self.assertEqual(result.exit_code, 0) finally: os.unlink(tmp_file) @@ -320,11 +323,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)] + black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"] ) finally: os.unlink(tmp_file) @@ -1842,6 +1846,34 @@ def test_find_project_root(self) -> None: self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) + @patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__) + def test_find_user_pyproject_toml_linux(self) -> None: + if system() == "Windows": + return + + # Test if XDG_CONFIG_HOME is checked + with TemporaryDirectory() as workspace: + tmp_user_config = Path(workspace) / "black" + with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}): + self.assertEqual( + black.find_user_pyproject_toml(), tmp_user_config.resolve() + ) + + # Test fallback for XDG_CONFIG_HOME + with patch.dict("os.environ"): + os.environ.pop("XDG_CONFIG_HOME", None) + fallback_user_config = Path("~/.config").expanduser() / "black" + self.assertEqual( + black.find_user_pyproject_toml(), fallback_user_config.resolve() + ) + + def test_find_user_pyproject_toml_windows(self) -> None: + if system() != "Windows": + return + + user_config_path = Path.home() / ".black" + self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve()) + def test_bpo_33660_workaround(self) -> None: if system() == "Windows": return From 125ed5b2601f0e74ded03b666132e3c37ae8af07 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 1 Apr 2021 09:41:55 -0700 Subject: [PATCH 167/680] Add a GitHub Action to build + Upload black to PyPI (#1848) * Add a GitHub Action to build + Upload black to PyPI - Build a wheel + sdist - Upload via twine using token stored in GitHub secrets --- .github/workflows/lint.yml | 7 ++----- .github/workflows/pypi_upload.yml | 31 +++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pypi_upload.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e01e9ade5e2..c46042d71b9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,17 +12,14 @@ jobs: github.repository runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.7] steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} + python-version: 3.7 - name: Install dependencies run: | diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml new file mode 100644 index 00000000000..5df91d92165 --- /dev/null +++ b/.github/workflows/pypi_upload.yml @@ -0,0 +1,31 @@ +name: pypi_upload + +on: + release: + types: created + +jobs: + build: + name: PyPI Upload + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + + - name: Install latest pip, setuptools, twine + wheel + run: | + python -m pip install --upgrade pip setuptools twine wheel + + - name: Build wheels + run: | + python setup.py bdist_wheel + python setup.py sdist + + - name: Upload to PyPI via Twine + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + twine upload --verbose -u '__token__' dist/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 292ed5727d9..48a7429c1f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: types_or: [python, pyi] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.1 + rev: 3.8.4 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] From 53a6216cd57955837e84f6ea82ca650c86320b03 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 1 Apr 2021 09:54:45 -0700 Subject: [PATCH 168/680] Add CONTRBUTING info about CHANGES.md requirement (#2073) Instruct contributors to add the change line to help save maintainer / releaser time --- .github/workflows/changelog.yml | 4 ++-- CONTRIBUTING.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 58a8c092edb..d7ee50558d3 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -16,6 +16,6 @@ jobs: - name: Grep CHANGES.md for PR number if: contains(github.event.pull_request.labels.*.name, 'skip news') != true run: | - grep -Pz "PR( |\n\s*)#${{ github.event.pull_request.number }}[^0-9]" CHANGES.md || \ - (echo "Please add 'PR #${{ github.event.pull_request.number }}' change line to CHANGES.md" && \ + 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" && \ exit 1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 571d870452b..8a3d8bf2830 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,6 +58,23 @@ $ tox -e fuzz $ black-primer [-k -w /tmp/black_test_repos] ``` +### News / Changelog Requirement + +`Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If +you feel this PR not require a changelog entry please state that in a comment and a +maintainer can add a `skip news` label to make the CI pass. Otherwise, please ensure you +have a line in the following format: + +```md +- `Black` is now more awesome (#X) +``` + +To workout X, checkout the latest issue and PR number and add 1. This is not perfect but +saves a lot of release overhead as now the releaser does not need to go back and workout +what to add to the `CHANGES.md` for each release. + +_Suggestions welcome on how this could be a better less invasive flow._ + ### Docs Testing If you make changes to docs, you can test they still build locally too. From 48dfda084a8c854a73c11b6c91c67deae5f86ca3 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:24:18 -0400 Subject: [PATCH 169/680] Push contributors to use Next PR Number (#2080) This is a tool of my own making. Right now our requirement to have the PR number in the changelog entry is pretty painful / annoying since the contributor either has to guess or add the # retroactively after the PR creation. This tool should make it way less painful by making it simple to get your PR number beforehand. --- CONTRIBUTING.md | 9 ++++----- docs/contributing_to_black.md | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a3d8bf2830..bd6ebfca114 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,11 +69,10 @@ have a line in the following format: - `Black` is now more awesome (#X) ``` -To workout X, checkout the latest issue and PR number and add 1. This is not perfect but -saves a lot of release overhead as now the releaser does not need to go back and workout -what to add to the `CHANGES.md` for each release. - -_Suggestions welcome on how this could be a better less invasive flow._ +To workout X, please use +[Next PR Number](https://ichard26.github.io/next-pr-number/?owner=psf&name=black). This +is not perfect but saves a lot of release overhead as now the releaser does not need to +go back and workout what to add to the `CHANGES.md` for each release. ### Docs Testing diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md index 562b43a76ac..b911b465afd 100644 --- a/docs/contributing_to_black.md +++ b/docs/contributing_to_black.md @@ -60,6 +60,22 @@ $ tox -e fuzz $ black-primer [-k -w /tmp/black_test_repos] ``` +### News / Changelog Requirement + +`Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If +you feel this PR not require a changelog entry please state that in a comment and a +maintainer can add a `skip news` label to make the CI pass. Otherwise, please ensure you +have a line in the following format: + +```md +- `Black` is now more awesome (#X) +``` + +To workout X, please use +[Next PR Number](https://ichard26.github.io/next-pr-number/?owner=psf&name=black). This +is not perfect but saves a lot of release overhead as now the releaser does not need to +go back and workout what to add to the `CHANGES.md` for each release. + ### Docs Testing If you make changes to docs, you can test they still build locally too. From 201b331e55948d187fc150101bebf5262e160290 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 1 Apr 2021 16:15:50 -0700 Subject: [PATCH 170/680] Run lint in latest python + update precommit (#2081) - Lets move to latest and greatest of lints --- .github/workflows/lint.yml | 2 -- .pre-commit-config.yaml | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c46042d71b9..2480a5ec131 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,6 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 - with: - python-version: 3.7 - name: Install dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48a7429c1f2..e461aa39c9d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,13 +13,13 @@ repos: types_or: [python, pyi] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.780 + rev: v0.812 hooks: - id: mypy exclude: ^docs/conf.py From e114ef5514e95cb9908b38c2397978f2070c1b0e Mon Sep 17 00:00:00 2001 From: Jakub Warczarek Date: Sun, 4 Apr 2021 16:21:33 +0200 Subject: [PATCH 171/680] Get rid of redundant spaces in docs (#2085) Thanks! --- README.md | 14 +++++++------- src/black/__init__.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b19e6e5e098..0bc0228b7d0 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ Options: split lines. --check Don't write the files back, just return the - status. Return code 0 means nothing would - change. Return code 1 means some files + status. Return code 0 means nothing would + change. Return code 1 means some files would be reformatted. Return code 123 means there was an internal error. @@ -119,20 +119,20 @@ Options: --include TEXT A regular expression that matches files and directories that should be included on - recursive searches. An empty value means + 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 + name. Use forward slashes for directories + on all platforms (Windows, too). Exclusions are calculated first, inclusions later. [default: \.pyi?$] --exclude TEXT A regular expression that matches files and directories that should be excluded on - recursive searches. An empty value means no + 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: /(\.direnv|\.eggs|\.git|\. + later. [default: /(\.direnv|\.eggs|\.git|\. hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu ild|buck-out|build|dist)/] diff --git a/src/black/__init__.py b/src/black/__init__.py index e85ffb37f88..431ee027270 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -445,8 +445,8 @@ def validate_regex( "--check", is_flag=True, help=( - "Don't write the files back, just return the status. Return code 0 means" - " nothing would change. Return code 1 means some files would be reformatted." + "Don't write the files back, just return the status. Return code 0 means" + " nothing would change. Return code 1 means some files would be reformatted." " Return code 123 means there was an internal error." ), ) @@ -472,9 +472,9 @@ def validate_regex( 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." + " 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, ) @@ -485,8 +485,8 @@ def validate_regex( callback=validate_regex, help=( "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). " + " 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." ), show_default=True, From 9cbf1f162261b64ebb150639b608be0c38f23e2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 11:07:57 -0700 Subject: [PATCH 172/680] Bump urllib3 from 1.26.3 to 1.26.4 (#2090) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 89 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 9cdcba2a0ea..3d908dc90f3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -569,20 +569,20 @@ }, "cryptography": { "hashes": [ - "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b", - "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336", - "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87", - "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7", - "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799", - "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b", - "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df", - "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0", - "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3", - "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724", - "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2", - "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964" - ], - "version": "==3.4.6" + "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", + "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", + "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", + "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", + "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", + "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", + "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", + "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", + "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", + "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", + "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", + "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + ], + "version": "==3.4.7" }, "distlib": { "hashes": [ @@ -624,10 +624,10 @@ }, "identify": { "hashes": [ - "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc", - "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3" + "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87", + "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034" ], - "version": "==1.5.14" + "version": "==2.2.2" }, "idna": { "hashes": [ @@ -643,6 +643,13 @@ ], "version": "==1.2.0" }, + "importlib-metadata": { + "hashes": [ + "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", + "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" + ], + "version": "==3.10.0" + }, "jeepney": { "hashes": [ "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", @@ -660,10 +667,10 @@ }, "keyring": { "hashes": [ - "sha256:9acb3e1452edbb7544822b12fd25459078769e560fa51f418b6d00afaa6178df", - "sha256:9f44660a5d4931bdc14c08a1d01ef30b18a7a8147380710d8c9f9531e1f6c3c0" + "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", + "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" ], - "version": "==22.0.1" + "version": "==23.0.1" }, "markupsafe": { "hashes": [ @@ -859,10 +866,10 @@ }, "pygments": { "hashes": [ - "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", - "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" + "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", + "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" ], - "version": "==2.8.0" + "version": "==2.8.1" }, "pyparsing": { "hashes": [ @@ -892,15 +899,23 @@ "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:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "version": "==5.4.1" }, @@ -1078,10 +1093,10 @@ }, "tqdm": { "hashes": [ - "sha256:2c44efa73b8914dba7807aefd09653ac63c22b5b4ea34f7a80973f418f1a3089", - "sha256:c23ac707e8e8aabb825e4d91f8e17247f9cc14b0d64dd9e97be0781e9e525bba" + "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", + "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" ], - "version": "==4.58.0" + "version": "==4.60.0" }, "twine": { "hashes": [ @@ -1138,17 +1153,18 @@ }, "urllib3": { "hashes": [ - "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", - "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "version": "==1.26.3" + "index": "pypi", + "version": "==1.26.4" }, "virtualenv": { "hashes": [ - "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", - "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" + "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107", + "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c" ], - "version": "==20.4.2" + "version": "==20.4.3" }, "webencodings": { "hashes": [ @@ -1206,6 +1222,13 @@ "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], "version": "==1.6.3" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "version": "==3.4.1" } } } From 8e0803e7e5acabdd28b80258f15d8aebf11fbb4c Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 7 Apr 2021 21:13:11 -0700 Subject: [PATCH 173/680] Add `black` Dockerfile (#1916) * Add `black` Dockerfile - Build a `black` container based on python3-slim - Test: `docker build --network-host --tag black .` ```console cooper-mbp1:black cooper$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE black latest 0c7636402ac2 4 seconds ago 332MB ``` Addresses part of #1914 * Build with colorama + d extra installs - Adds ~9mb * Combine all build commands in 1 run - Combine the commands all into 1 RUN to down the size - Get to 65.98 MB image size now: https://hub.docker.com/repository/docker/cooperlees/black/tags?page=1&ordering=last_updated * Add rm -r of /var/lib/apt/lists/* + save 10mb down to 55mb --- Dockerfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..9542479eca5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3-slim + +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 .[colorama,d] \ + && rm -rf /src \ + && apt remove -y git \ + && apt autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +CMD ["black"] From 4d36304a2c70a5b6e39cdabf404fe72186fc8202 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Apr 2021 10:25:37 -0700 Subject: [PATCH 174/680] Fix error from upcoming typeshed change (#2096) See python/typeshed#5190 --- 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 431ee027270..54d6edf760a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -847,7 +847,7 @@ async def schedule_formatting( ): src for src in sorted(sources) } - pending: Iterable["asyncio.Future[bool]"] = tasks.keys() + pending = tasks.keys() try: loop.add_signal_handler(signal.SIGINT, cancel, pending) loop.add_signal_handler(signal.SIGTERM, cancel, pending) From e4003c2c439fc08a407d3024a195c010f03c4173 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Sat, 10 Apr 2021 08:07:34 -0400 Subject: [PATCH 175/680] Exclude venv directory by default (#1683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- CHANGES.md | 2 ++ README.md | 4 ++-- docs/installation_and_usage.md | 4 ++-- src/black/__init__.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 97a3be33c93..6340f60ae59 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,8 @@ - PR #2053: Python 2 support is now optional, install with `python3 -m pip install black[python2]` to maintain support. +- Exclude `venv` directory by default (#1683) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/README.md b/README.md index 0bc0228b7d0..ed9f105f5aa 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ Options: directories on all platforms (Windows, too). Exclusions are calculated first, inclusions later. [default: /(\.direnv|\.eggs|\.git|\. - hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu - ild|buck-out|build|dist)/] + hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.sv + n|_build|buck-out|build|dist)/] --extend-exclude TEXT Like --exclude, but adds additional files and directories on top of the excluded diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index bb554eb6744..fcde49f4e3a 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -87,8 +87,8 @@ Options: directories on all platforms (Windows, too). Exclusions are calculated first, inclusions later. [default: /(\.direnv|\.eggs|\.git|\. - hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu - ild|buck-out|build|dist)/] + hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.sv + n|_build|buck-out|build|dist)/] --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even diff --git a/src/black/__init__.py b/src/black/__init__.py index 54d6edf760a..0065beafe0e 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -82,7 +82,7 @@ import colorama # noqa: F401 DEFAULT_LINE_LENGTH = 88 -DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.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)/" # noqa: B950 DEFAULT_INCLUDES = r"\.pyi?$" CACHE_DIR = Path(user_cache_dir("black", version=__version__)) STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" From 2116eca51f1108ebc924185c87bc363d8e0329b3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 11 Apr 2021 07:49:42 -0700 Subject: [PATCH 176/680] fix typing issue around lru_cache arguments (#2098) This was found by python/mypy#10308 --- src/black/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 0065beafe0e..2bb42a470ea 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -302,7 +302,7 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b return all(feature in VERSION_TO_FEATURES[version] for version in target_versions) -def find_pyproject_toml(path_search_start: Iterable[str]) -> Optional[str]: +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_pyproject_toml = path_project_root / "pyproject.toml" @@ -6214,7 +6214,7 @@ def gen_python_files( @lru_cache() -def find_project_root(srcs: Iterable[str]) -> Path: +def find_project_root(srcs: Tuple[str, ...]) -> Path: """Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories From ea4e714b9a3b636afd8e81c5c5c916fcf8e6ed42 Mon Sep 17 00:00:00 2001 From: Simon <32608483+J-Exodus@users.noreply.github.com> Date: Mon, 12 Apr 2021 07:00:03 +1000 Subject: [PATCH 177/680] Added not formatting files in gitignore (psf#1682) (#1734) --- docs/pyproject_toml.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 77c12b76be9..ed88f37aa4a 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -48,6 +48,8 @@ You can also explicitly specify the path to a particular file that you want with If you're running with `--verbose`, you will see a blue message if a file was found and used. +Files listed within a projects `.gitignore` file will not be formatted by _Black_. + Please note `blackd` will not use `pyproject.toml` configuration. ## Configuration format From d960d5d238bc8c73010a3ca5a9f316678ad91e6f Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 11 Apr 2021 23:41:22 +0200 Subject: [PATCH 178/680] Remove NBSP at the beginning of comments (#2092) Closes #2091 --- CHANGES.md | 2 ++ src/black/__init__.py | 7 ++++ tests/data/comments7.py | 7 +++- tests/data/comments_non_breaking_space.py | 44 +++++++++++++++++++++++ tests/test_format.py | 1 + 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/data/comments_non_breaking_space.py diff --git a/CHANGES.md b/CHANGES.md index 6340f60ae59..12e8d453277 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ #### _Black_ +- `Black` now cleans up leading non-breaking spaces in comments (#2092) + - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) diff --git a/src/black/__init__.py b/src/black/__init__.py index 2bb42a470ea..36716474e8c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2709,6 +2709,13 @@ def make_comment(content: str) -> str: if content[0] == "#": content = content[1:] + NON_BREAKING_SPACE = " " + if ( + content + and content[0] == NON_BREAKING_SPACE + and not content.lstrip().startswith("type:") + ): + content = " " + content[1:] # Replace NBSP by a simple space if content and content[0] not in " !:#'%": content = " " + content return "#" + content diff --git a/tests/data/comments7.py b/tests/data/comments7.py index 0e2bd35bf55..ca9d7c62b21 100644 --- a/tests/data/comments7.py +++ b/tests/data/comments7.py @@ -129,6 +129,8 @@ def test_fails_invalid_post_data( ): ... +square = Square(4) # type: Optional[Square] + # output from .config import ( @@ -263,4 +265,7 @@ class C: def test_fails_invalid_post_data( self, pyramid_config, db_request, post_data, message ): - ... \ No newline at end of file + ... + + +square = Square(4) # type: Optional[Square] diff --git a/tests/data/comments_non_breaking_space.py b/tests/data/comments_non_breaking_space.py new file mode 100644 index 00000000000..e17c3f4ca39 --- /dev/null +++ b/tests/data/comments_non_breaking_space.py @@ -0,0 +1,44 @@ +from .config import ( ConfigTypeAttributes, Int, Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) + +result = 1 # A simple comment +result = ( 1, ) # Another one + +result = 1 # type: ignore +result = 1# This comment is talking about type: ignore +square = Square(4) # type: Optional[Square] + +def function(a:int=42): + """ This docstring is already formatted + a + b + """ + #  There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass + +# output +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) + +result = 1 # A simple comment +result = (1,) # Another one + +result = 1 #  type: ignore +result = 1 # This comment is talking about type: ignore +square = Square(4) #  type: Optional[Square] + + +def function(a: int = 42): + """This docstring is already formatted + a + b + """ + # There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass diff --git a/tests/test_format.py b/tests/test_format.py index e0cb0b74195..eabec8c7219 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -27,6 +27,7 @@ "comments5", "comments6", "comments7", + "comments_non_breaking_space", "comment_after_escaped_newline", "composition", "composition_no_trailing_comma", From 6d0bdc2f38d57ddf07e7d127215d51f8553997f9 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Mon, 12 Apr 2021 15:18:11 +0200 Subject: [PATCH 179/680] Add missing instructions to make test passed (#2100) --- .github/ISSUE_TEMPLATE/bug_report.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e652f17c94b..f4a438622aa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,7 +28,8 @@ assignees: "" 2. 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 .`; + - run `pip install -e .[d,python2]`; + - run `pip install -r test_requirements.txt` - make sure it's sane by running `python -m unittest`; and - run `black` like you did last time. From 9eb29a6d4733c0e467ba5c1c54361f097cc1c305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 16 Apr 2021 19:41:19 +0200 Subject: [PATCH 180/680] Fix small comment typo (#2112) We probably don't need to fall back on "polling" when setting up an asyncio loop --- 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 36716474e8c..bb8f8c7422a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -784,7 +784,7 @@ def reformat_many( except (ImportError, 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 ThreadPollExecutor with just a single worker (more workers would not do us + # 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) From 7b4ca4bd90c148d72548048f5109537a555aaf77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Mon, 19 Apr 2021 00:18:39 +0300 Subject: [PATCH 181/680] Instructions on documenting code style modifications (#2109) --- CONTRIBUTING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd6ebfca114..f2a4ee20c99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,12 @@ To workout X, please use is not perfect but saves a lot of release overhead as now the releaser does not need to go back and workout what to add to the `CHANGES.md` for each release. +### Style Changes + +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. + ### Docs Testing If you make changes to docs, you can test they still build locally too. From 1fc3215e8c8856094b20d497e4e0e3e547ed38eb Mon Sep 17 00:00:00 2001 From: Mark Bell Date: Thu, 22 Apr 2021 16:23:41 +0100 Subject: [PATCH 182/680] Make black remove leading and trailing spaces from one-line docstrings (#1740) Fixes #1738. Fixes #1812. Previously, Black removed leading and trailing spaces in multiline docstrings but failed to remove them from one-line docstrings. --- CHANGES.md | 3 +++ src/black/__init__.py | 40 ++++++++++++++++++++++++------------ src/black_primer/primer.json | 10 ++++----- tests/data/docstring.py | 36 +++++++++++++++++++++++++++++++- tox.ini | 2 +- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 12e8d453277..f911fdf77ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ #### _Black_ +- `Black` now processes one-line docstrings by stripping leading and trailing spaces, + and adding a padding space when needed to break up """". (#1740) + - `Black` now cleans up leading non-breaking spaces in comments (#2092) - `Black` now respects `--skip-string-normalization` when normalizing multiline diff --git a/src/black/__init__.py b/src/black/__init__.py index bb8f8c7422a..efa82f41160 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2153,16 +2153,35 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. prefix = get_string_prefix(leaf.value) - lead_len = len(prefix) + 3 - tail_len = -3 - indent = " " * 4 * self.current_line.depth - docstring = fix_docstring(leaf.value[lead_len:tail_len], indent) + docstring = leaf.value[len(prefix) :] # Remove the prefix + quote_char = docstring[0] + # A natural way to remove the outer quotes is to do: + # docstring = docstring.strip(quote_char) + # but that breaks on """""x""" (which is '""x'). + # So we actually need to remove the first character and the next two + # characters but only if they are the same as the first. + quote_len = 1 if docstring[1] != quote_char else 3 + docstring = docstring[quote_len:-quote_len] + + if is_multiline_string(leaf): + indent = " " * 4 * self.current_line.depth + docstring = fix_docstring(docstring, indent) + else: + docstring = docstring.strip() + if docstring: - if leaf.value[lead_len - 1] == docstring[0]: + # Add some padding if the docstring starts / ends with a quote mark. + if docstring[0] == quote_char: docstring = " " + docstring - if leaf.value[tail_len + 1] == docstring[-1]: + if docstring[-1] == quote_char: docstring = docstring + " " - leaf.value = leaf.value[0:lead_len] + docstring + leaf.value[tail_len:] + else: + # Add some padding if the docstring is empty. + docstring = " " + + # We could enforce triple quotes at this point. + quote = quote_char * quote_len + leaf.value = prefix + quote + docstring + quote yield from self.visit_default(leaf) @@ -6113,7 +6132,7 @@ def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]: @lru_cache() def get_gitignore(root: Path) -> PathSpec: - """ Return a PathSpec matching gitignore content if present.""" + """Return a PathSpec matching gitignore content if present.""" gitignore = root / ".gitignore" lines: List[str] = [] if gitignore.is_file(): @@ -6953,11 +6972,6 @@ def patched_main() -> None: def is_docstring(leaf: Leaf) -> bool: - if not is_multiline_string(leaf): - # For the purposes of docstring re-indentation, we don't need to do anything - # with single-line docstrings. - return False - if prev_siblings_are( leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] ): diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 32df01571a7..3a1fae090a0 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -3,7 +3,7 @@ "projects": { "aioexabgp": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, "py_versions": ["all"] @@ -17,7 +17,7 @@ }, "bandersnatch": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pypa/bandersnatch.git", "long_checkout": false, "py_versions": ["all"] @@ -84,7 +84,7 @@ }, "ptr": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/facebookincubator/ptr.git", "long_checkout": false, "py_versions": ["all"] @@ -98,14 +98,14 @@ }, "sqlalchemy": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", "long_checkout": false, "py_versions": ["all"] }, "tox": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/tox-dev/tox.git", "long_checkout": false, "py_versions": ["all"] diff --git a/tests/data/docstring.py b/tests/data/docstring.py index 5c6985d0e08..74532b2b91d 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -102,6 +102,23 @@ def and_this(): "hey yah"''' +def empty(): + ''' + + + + + ''' + + +def oneline_empty(): + ''' ''' + + +def single_quotes(): + 'testing' + + def believe_it_or_not_this_is_in_the_py_stdlib(): ''' "hey yah"''' @@ -110,6 +127,8 @@ def ignored_docstring(): """a => \ b""" +def single_line_docstring_with_whitespace(): + """ This should be stripped """ def docstring_with_inline_tabs_and_space_indentation(): """hey @@ -134,7 +153,6 @@ def docstring_with_inline_tabs_and_tab_indentation(): line ends with some tabs """ pass - # output @@ -241,6 +259,18 @@ def and_this(): "hey yah"''' +def empty(): + """ """ + + +def oneline_empty(): + """ """ + + +def single_quotes(): + "testing" + + def believe_it_or_not_this_is_in_the_py_stdlib(): ''' "hey yah"''' @@ -251,6 +281,10 @@ def ignored_docstring(): b""" +def single_line_docstring_with_whitespace(): + """This should be stripped""" + + def docstring_with_inline_tabs_and_space_indentation(): """hey diff --git a/tox.ini b/tox.ini index 9bb809abe41..71b02e920c7 100644 --- a/tox.ini +++ b/tox.ini @@ -23,4 +23,4 @@ commands = pip install -e .[d] coverage erase coverage run fuzz.py - coverage report \ No newline at end of file + coverage report From 5316bbff0e9db989db98b14761ce2c299a05895b Mon Sep 17 00:00:00 2001 From: CiderMan Date: Thu, 22 Apr 2021 16:40:51 +0100 Subject: [PATCH 183/680] Handle Docstrings as bytes + strip all whitespace (#2037) (fixes #1844, fixes #1923, fixes #1851, fixes #2002, fixes #2103) --- CHANGES.md | 3 +++ src/black/__init__.py | 14 ++++++++++++-- tests/test_black.py | 20 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f911fdf77ae..2999abfff4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,6 +52,9 @@ - Exclude `venv` directory by default (#1683) +- Fixed "Black produced code that is not equivalent to the source" when formatting + Python 2 docstrings (#2037) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/src/black/__init__.py b/src/black/__init__.py index efa82f41160..5c9ab751650 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -6466,12 +6466,22 @@ def _stringify_ast( # 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) + and isinstance(value, (str, bytes)) ): - normalized = re.sub(r" *\n[ \t]*", "\n", value).strip() + 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() else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" diff --git a/tests/test_black.py b/tests/test_black.py index c603233efc4..201edfa2de5 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1918,6 +1918,26 @@ def test_bpo_2142_workaround(self) -> None: actual = diff_header.sub(DETERMINISTIC_HEADER, actual) self.assertEqual(actual, expected) + 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 = CliRunner().invoke( + black.main, + ["-", "-q", "--target-version=py27"], + input=BytesIO(source), + ) + + self.assertEqual(result.exit_code, 0) + actual = result.output + self.assertFormatEqual(actual, expected) + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 368f043f138112f63ff521c3481993c589eb7508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Thu, 22 Apr 2021 20:37:27 +0300 Subject: [PATCH 184/680] Document experimental string processing and docstring indentation (#2106) --- docs/the_black_code_style.md | 15 +++++++++++++++ tests/data/docstring.py | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 1cc591b8031..67ccb8aab11 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -289,6 +289,21 @@ 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. +As an experimental option, _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. To enable experimental +string processing, pass `--experimental-string-processing` on the command line. 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 +end of the docstring are removed. All leading tabs are converted to spaces, but tabs +inside text are preserved. Whitespace leading and trailing one-line docstrings is +removed. The quotations of an empty docstring are separated with one space. + ### Numeric literals _Black_ standardizes most numeric literals to use lowercase letters for the syntactic diff --git a/tests/data/docstring.py b/tests/data/docstring.py index 74532b2b91d..9e1c2441e0e 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -115,6 +115,10 @@ def oneline_empty(): ''' ''' +def oneline_nothing(): + """""" + + def single_quotes(): 'testing' @@ -267,6 +271,10 @@ def oneline_empty(): """ """ +def oneline_nothing(): + """ """ + + def single_quotes(): "testing" From 773e4a22d58be1f1aa82df7ad9a0f4c4e1328c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 25 Apr 2021 19:04:15 +0200 Subject: [PATCH 185/680] Revert "Use lowercase hex numbers fixes #1692 (#1775)" This reverts commit 7d032fa848c8910007a0a41c1ba61d70d2846f48. --- .gitignore | 3 +-- src/black/__init__.py | 9 ++------- src/black_primer/primer.json | 10 +++++----- src/blib2to3/pytree.py | 2 +- tests/data/numeric_literals.py | 4 ++-- tests/data/numeric_literals_py2.py | 4 ++-- tests/data/numeric_literals_skip_underscores.py | 6 +++--- 7 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 30225ec7764..3207e72ae28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.venv .coverage _build .DS_Store @@ -16,4 +15,4 @@ src/_black_version.py .dmypy.json *.swp .hypothesis/ -venv/ +venv/ \ No newline at end of file diff --git a/src/black/__init__.py b/src/black/__init__.py index 5c9ab751650..0a893aa80fe 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5351,15 +5351,10 @@ def normalize_numeric_literal(leaf: Leaf) -> None: def format_hex(text: str) -> str: """ - Formats a hexadecimal string like "0x12b3" - - Uses lowercase because of similarity between "B" and "8", which - can cause security issues. - see: https://github.com/psf/black/issues/1692 + Formats a hexadecimal string like "0x12B3" """ - before, after = text[:2], text[2:] - return f"{before}{after.lower()}" + return f"{before}{after.upper()}" def format_scientific_notation(text: str) -> str: diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 3a1fae090a0..7f7c3b0f37b 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -10,7 +10,7 @@ }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] @@ -47,7 +47,7 @@ }, "hypothesis": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] @@ -63,7 +63,7 @@ }, "pillow": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/python-pillow/Pillow.git", "long_checkout": false, "py_versions": ["all"] @@ -77,7 +77,7 @@ }, "pyramid": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/Pylons/pyramid.git", "long_checkout": false, "py_versions": ["all"] @@ -112,7 +112,7 @@ }, "virtualenv": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pypa/virtualenv.git", "long_checkout": false, "py_versions": ["all"] diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 0c074f6a4ac..7843467e012 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -33,7 +33,7 @@ import sys from io import StringIO -HUGE: int = 0x7fffffff # maximum repeat count, default max +HUGE: int = 0x7FFFFFFF # maximum repeat count, default max _type_reprs: Dict[int, Union[Text, int]] = {} diff --git a/tests/data/numeric_literals.py b/tests/data/numeric_literals.py index 06b7f7758ee..254da68d330 100644 --- a/tests/data/numeric_literals.py +++ b/tests/data/numeric_literals.py @@ -12,7 +12,7 @@ x = 123456789E123456789 x = 123456789J x = 123456789.123456789J -x = 0Xb1aCc +x = 0XB1ACC x = 0B1011 x = 0O777 x = 0.000000006 @@ -36,7 +36,7 @@ x = 123456789e123456789 x = 123456789j x = 123456789.123456789j -x = 0xb1acc +x = 0xB1ACC x = 0b1011 x = 0o777 x = 0.000000006 diff --git a/tests/data/numeric_literals_py2.py b/tests/data/numeric_literals_py2.py index 8b2c7faa306..8f85c43f265 100644 --- a/tests/data/numeric_literals_py2.py +++ b/tests/data/numeric_literals_py2.py @@ -3,7 +3,7 @@ x = 123456789L x = 123456789l x = 123456789 -x = 0xB1aCc +x = 0xb1acc # output @@ -13,4 +13,4 @@ x = 123456789L x = 123456789L x = 123456789 -x = 0xb1acc +x = 0xB1ACC diff --git a/tests/data/numeric_literals_skip_underscores.py b/tests/data/numeric_literals_skip_underscores.py index f83e23312f2..e345bb90276 100644 --- a/tests/data/numeric_literals_skip_underscores.py +++ b/tests/data/numeric_literals_skip_underscores.py @@ -3,7 +3,7 @@ x = 123456789 x = 1_2_3_4_5_6_7 x = 1E+1 -x = 0xb1AcC +x = 0xb1acc x = 0.00_00_006 x = 12_34_567J x = .1_2 @@ -16,8 +16,8 @@ x = 123456789 x = 1_2_3_4_5_6_7 x = 1e1 -x = 0xb1acc +x = 0xB1ACC x = 0.00_00_006 x = 12_34_567j x = 0.1_2 -x = 1_2.0 +x = 1_2.0 \ No newline at end of file From ea9fefb359b86208ad20c944c3c420e6c3fb2c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 25 Apr 2021 19:42:02 +0200 Subject: [PATCH 186/680] Re-add .venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3207e72ae28..f487d96cb3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.venv .coverage _build .DS_Store From 5e09fa07f5db113c9b0858da15eff6bb31fe2417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 25 Apr 2021 19:44:06 +0200 Subject: [PATCH 187/680] Fix primer config --- src/black_primer/primer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 7f7c3b0f37b..1dd380b3cb4 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -10,7 +10,7 @@ }, "attrs": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-attrs/attrs.git", "long_checkout": false, "py_versions": ["all"] @@ -63,7 +63,7 @@ }, "pillow": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-pillow/Pillow.git", "long_checkout": false, "py_versions": ["all"] @@ -77,7 +77,7 @@ }, "pyramid": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/Pylons/pyramid.git", "long_checkout": false, "py_versions": ["all"] @@ -91,7 +91,7 @@ }, "pytest": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pytest-dev/pytest.git", "long_checkout": false, "py_versions": ["all"] @@ -112,7 +112,7 @@ }, "virtualenv": { "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pypa/virtualenv.git", "long_checkout": false, "py_versions": ["all"] From 8672af35f052a636545e38110f0419ea92aeca0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 25 Apr 2021 20:15:54 +0200 Subject: [PATCH 188/680] Work around stability errors due to optional trailing commas (#2126) Optional trailing commas put by Black become magic trailing commas on another pass of the tool. Since they are influencing formatting around optional parentheses, on rare occasions the tool changes its mind in terms of putting parentheses or not. Ideally this would never be the case but sadly the decision to put optional parentheses or not (which looks at pre-existing "magic" trailing commas) is happening around the same time as the decision to put an optional trailing comma. Untangling the two proved to be impractically difficult. This shameful workaround uses the fact that the formatting instability introduced by magic trailing commas is deterministic: if the optional trailing comma becoming a pre-existing "magic" trailing comma changes formatting, the second pass becomes stable since there is no variable factor anymore on pass 3, 4, and so on. For most files, this will introduce no performance penalty since `--safe` is already re-formatting everything twice to ensure formatting stability. We're using this result and if all's good, the behavior is equivalent. If there is a difference, we treat the second result as the binding one, and check its sanity again. --- CHANGES.md | 4 ++++ src/black/__init__.py | 24 +++++++++++++++++------- tests/test_black.py | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2999abfff4e..0ca0b84f6d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,10 @@ #### _Black_ +- Fixed a rare but annoying formatting instability created by the combination of + optional trailing commas inserted by `Black` and optional parentheses looking at + pre-existing "magic" trailing commas (#2126) + - `Black` now processes one-line docstrings by stripping leading and trailing spaces, and adding a padding space when needed to break up """". (#1740) diff --git a/src/black/__init__.py b/src/black/__init__.py index 0a893aa80fe..08267d50f89 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1016,7 +1016,17 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo if not fast: assert_equivalent(src_contents, dst_contents) - assert_stable(src_contents, dst_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. + 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`. return dst_contents @@ -6484,7 +6494,7 @@ def _stringify_ast( yield f"{' ' * depth}) # /{node.__class__.__name__}" -def assert_equivalent(src: str, dst: str) -> None: +def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: """Raise AssertionError if `src` and `dst` aren't equivalent.""" try: src_ast = parse_ast(src) @@ -6499,9 +6509,9 @@ def assert_equivalent(src: str, dst: str) -> 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: {exc}. Please report a bug" - " on https://github.com/psf/black/issues. This invalid output might be" - f" helpful: {log}" + f"INTERNAL ERROR: Black produced invalid code on pass {pass_num}: {exc}. " + "Please report a bug on https://github.com/psf/black/issues. " + f"This invalid output might be helpful: {log}" ) from None src_ast_str = "\n".join(_stringify_ast(src_ast)) @@ -6510,8 +6520,8 @@ 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" - " source. Please report a bug on https://github.com/psf/black/issues. " - f" This diff might be helpful: {log}" + f" source on pass {pass_num}. 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_black.py b/tests/test_black.py index 201edfa2de5..8733b7a63ad 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -254,6 +254,24 @@ def test_trailing_comma_optional_parens_stability3(self) -> None: 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) + @patch("black.dump_to_file", dump_to_stderr) def test_pep_572(self) -> None: source, expected = read_data("pep_572") From e5490e96232e301e9eed971984bb75f2f87475b0 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 25 Apr 2021 11:27:35 -0700 Subject: [PATCH 189/680] Add Docker Github Action (#2086) * Add Docker Github Action - Build and upload arm64 + amd64 black images on push to master This will need a `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets set by someone with access. * Change tag to push to pyfound/black repository. Thanks @ewdurbin --- .github/workflows/docker.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000000..40b3d07736b --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,33 @@ +name: docker + +on: + push: + branches: + - "master" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: pyfound/black:latest + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} From eee949e286363384b25426b9ce461d31a5842ebf Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 25 Apr 2021 14:51:13 -0700 Subject: [PATCH 190/680] Docker CI: Add missed Checkout step (#2128) - Reading my error others hit it by forgetting this Checkoutstep too so trying the fix - e.g. https://github.com/docker/build-push-action/issues/179 - Makes sense it's needed --- .github/workflows/docker.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 40b3d07736b..09390130b3d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,6 +9,9 @@ jobs: docker: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU uses: docker/setup-qemu-action@v1 From de7187a823bd701ecb75c62bca0d7bd00076108a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 25 Apr 2021 17:52:23 -0400 Subject: [PATCH 191/680] Issue 1629 has been closed, let's celebrate! (#2127) --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0ca0b84f6d1..65d037394ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,8 @@ - Fixed a rare but annoying formatting instability created by the combination of optional trailing commas inserted by `Black` and optional parentheses looking at - pre-existing "magic" trailing commas (#2126) + pre-existing "magic" trailing commas. This fixes issue #1629 and all of its many many + duplicates. (#2126) - `Black` now processes one-line docstrings by stripping leading and trailing spaces, and adding a padding space when needed to break up """". (#1740) From 67d5532c3392280de0ce717a1ab728eca2beb698 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 25 Apr 2021 15:12:27 -0700 Subject: [PATCH 192/680] Update CHANGES.md for release (#2129) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 65d037394ad..76653853d90 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ ## Change Log -### Unreleased +### 21.4b0 #### _Black_ From 65f0ea61599dc6d2be95ee961c42a55247130c49 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 25 Apr 2021 20:48:04 -0700 Subject: [PATCH 193/680] Remove Lowercase Hex (PR #1692) from CHANGES.md (#2133) - It was reverted to not cause so much diff churn on millions of lines of code - Fix primer config for projects that should now pass --- CHANGES.md | 2 -- src/black_primer/primer.json | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 76653853d90..e313a44474d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,8 +37,6 @@ - Added parsing support for unparenthesized tuples and yield expressions in annotated assignments (#1835) -- use lowercase hex strings (#1692) - - added `--extend-exclude` argument (PR #2005) - speed up caching by avoiding pathlib (#1950) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 1dd380b3cb4..7a6f5abf408 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -3,7 +3,7 @@ "projects": { "aioexabgp": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, "py_versions": ["all"] @@ -17,7 +17,7 @@ }, "bandersnatch": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pypa/bandersnatch.git", "long_checkout": false, "py_versions": ["all"] From 0a833b4b14953f98e81d632281a75318faa66170 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 25 Apr 2021 22:46:48 -0700 Subject: [PATCH 194/680] fix magic comma and experimental string cache flags (#2131) * fix magic comma and experimental string cache flags * more changelog * Update CHANGES.md Co-authored-by: Cooper Lees * fix tests Co-authored-by: Cooper Lees --- CHANGES.md | 8 ++++++++ src/black/__init__.py | 4 +++- tests/test_black.py | 7 ++++--- tests/test_format.py | 37 ++++++++++++++++++++++--------------- tests/util.py | 2 +- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e313a44474d..aa08396e491 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ ## Change Log +### Unreleased + +#### _Black_ + +- Reflect the `--skip-magic-trailing-comma` and `--experimental-string-processing` flags + in the name of the cache file. Without this fix, changes in these flags would not take + effect if the cache had already been populated. (#2131) + ### 21.4b0 #### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 08267d50f89..00d3729c835 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -273,9 +273,9 @@ class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) line_length: int = DEFAULT_LINE_LENGTH string_normalization: bool = True + is_pyi: bool = False magic_trailing_comma: bool = True experimental_string_processing: bool = False - is_pyi: bool = False def get_cache_key(self) -> str: if self.target_versions: @@ -290,6 +290,8 @@ def get_cache_key(self) -> str: str(self.line_length), str(int(self.string_normalization)), str(int(self.is_pyi)), + str(int(self.magic_trailing_comma)), + str(int(self.experimental_string_processing)), ] return ".".join(parts) diff --git a/tests/test_black.py b/tests/test_black.py index 8733b7a63ad..88ea31777fb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -379,11 +379,12 @@ 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") - actual = fs(source) + 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, DEFAULT_MODE) - mode = replace(DEFAULT_MODE, string_normalization=False) + black.assert_stable(source, actual, 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) diff --git a/tests/test_format.py b/tests/test_format.py index eabec8c7219..e1335aedf43 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -16,7 +16,6 @@ SIMPLE_CASES = [ "beginning_backslash", "bracketmatch", - "cantfit", "class_blank_parentheses", "class_methods_new_line", "collections", @@ -26,7 +25,6 @@ "comments4", "comments5", "comments6", - "comments7", "comments_non_breaking_space", "comment_after_escaped_newline", "composition", @@ -48,11 +46,7 @@ "function2", "function_trailing_comma", "import_spacing", - "long_strings", - "long_strings__edge_case", - "long_strings__regression", "numeric_literals_py2", - "percent_precedence", "python2", "python2_unicode_literals", "remove_parens", @@ -62,6 +56,15 @@ "tupleassign", ] +EXPERIMENTAL_STRING_PROCESSING_CASES = [ + "cantfit", + "comments7", + "long_strings", + "long_strings__edge_case", + "long_strings__regression", + "percent_precedence", +] + SOURCES = [ "tests/test_black.py", @@ -86,19 +89,23 @@ class TestSimpleFormat(BlackBaseTestCase): @parameterized.expand(SIMPLE_CASES) @patch("black.dump_to_file", dump_to_stderr) def test_simple_format(self, filename: str) -> None: - source, expected = read_data(filename) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) + 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 - source, expected = read_data(str(path), data=False) - actual = fs(source, mode=DEFAULT_MODE) + self.check_file(str(path), DEFAULT_MODE, data=False) + self.assertFalse(ff(path)) + + 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) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - self.assertFalse(ff(path)) + black.assert_stable(source, actual, mode) diff --git a/tests/util.py b/tests/util.py index da65ed0cc93..ad9866975ac 100644 --- a/tests/util.py +++ b/tests/util.py @@ -12,7 +12,7 @@ DETERMINISTIC_HEADER = "[Deterministic header]" -DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +DEFAULT_MODE = black.Mode() ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) fs = partial(black.format_str, mode=DEFAULT_MODE) From db30456916ca733d011a2774e66beb3dd7064e14 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:28:42 -0400 Subject: [PATCH 195/680] Don't strip parens in assert / return with assign expr (#2143) Black would previously strip the parenthesis away from statements like this these ones: assert (spam := 12 + 1) return (cheese := 1 - 12) Which happens to be invalid code. Now before making the parenthesis invisible, Black checks if the assignment expression's parent is an assert stamtment, aborting if True. Raise, yield, and await are already handled fine. I added a bunch of test cases from the PEP defining asssignment expressions (PEP 572). --- CHANGES.md | 3 ++ src/black/__init__.py | 7 +++- tests/data/pep_572_remove_parens.py | 52 +++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index aa08396e491..9fe6b7e3fa4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,9 @@ in the name of the cache file. Without this fix, changes in these flags would not take effect if the cache had already been populated. (#2131) +- Don't remove necessary parentheses from assignment expression containing assert / + return statements. (#2143) + ### 21.4b0 #### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 00d3729c835..99afc7d3c50 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5613,7 +5613,12 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: return False if is_walrus_assignment(node): - if parent.type in [syms.annassign, syms.expr_stmt]: + if parent.type in [ + syms.annassign, + syms.expr_stmt, + syms.assert_stmt, + syms.return_stmt, + ]: return False first = node.children[0] diff --git a/tests/data/pep_572_remove_parens.py b/tests/data/pep_572_remove_parens.py index 04cc75bc36c..9a804859a09 100644 --- a/tests/data/pep_572_remove_parens.py +++ b/tests/data/pep_572_remove_parens.py @@ -17,6 +17,32 @@ a, b = (test := (1, 2)) +# see also https://github.com/psf/black/issues/2139 +assert (foo := 42 - 12) + +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + ... + + +def foo2(answer: (p := 42) = 5): + ... + + +lambda: (x := 1) + +# we don't touch expressions in f-strings but if we do one day, don't break 'em +f'{(x:=10)}' + + +def a(): + return (x := 3) + await (b := 1) + yield (a := 2) + raise (c := 3) + # output if foo := 0: pass @@ -36,3 +62,29 @@ test: int = (test2 := 2) a, b = (test := (1, 2)) + +# see also https://github.com/psf/black/issues/2139 +assert (foo := 42 - 12) + +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + ... + + +def foo2(answer: (p := 42) = 5): + ... + + +lambda: (x := 1) + +# we don't touch expressions in f-strings but if we do one day, don't break 'em +f"{(x:=10)}" + + +def a(): + return (x := 3) + await (b := 1) + yield (a := 2) + raise (c := 3) From 97c24664c5bc251f7b67756b8b184962ca44b6d9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 26 Apr 2021 13:26:03 -0400 Subject: [PATCH 196/680] Symlink docs/change_log.md to CHANGES.md, don't copy (#2146) Super duper janky stopgap fix until I get my documentation reorganization work done and merged --- docs/change_log.md | 537 +-------------------------------------------- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 537 deletions(-) mode change 100644 => 120000 docs/change_log.md diff --git a/docs/change_log.md b/docs/change_log.md deleted file mode 100644 index 01c27553fca..00000000000 --- a/docs/change_log.md +++ /dev/null @@ -1,536 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CHANGES.md" - -## Change Log - -### Unreleased - -#### _Black_ - -- `Black` now respects `--skip-string-normalization` when normalizing multiline - docstring quotes (#1637) - -- `Black` no longer removes all empty lines between non-function code and decorators - when formatting typing stubs. Now `Black` enforces a single empty line. (#1646) - -- `Black` no longer adds an incorrect space after a parenthesized assignment expression - in if/while statements (#1655) - -- fixed a crash when PWD=/ on POSIX (#1631) - -- fixed "I/O operation on closed file" when using --diff (#1664) - -- Prevent coloured diff output being interleaved with multiple files (#1673) - -- Added support for PEP 614 relaxed decorator syntax on python 3.9 (#1711) - -- Added parsing support for unparenthesized tuples and yield expressions in annotated - assignments (#1835) - -- use lowercase hex strings (#1692) - -- added `--extend-exclude` argument (#1571) - -#### _Packaging_ - -- Self-contained native _Black_ binaries are now provided for releases via GitHub - Releases (#1743) - -### 20.8b1 - -#### _Packaging_ - -- explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions - older than 7.0 - -### 20.8b0 - -#### _Black_ - -- re-implemented support for explicit trailing commas: now it works consistently within - any bracket pair, including nested structures (#1288 and duplicates) - -- `Black` now reindents docstrings when reindenting code around it (#1053) - -- `Black` now shows colored diffs (#1266) - -- `Black` is now packaged using 'py3' tagged wheels (#1388) - -- `Black` now supports Python 3.8 code, e.g. star expressions in return statements - (#1121) - -- `Black` no longer normalizes capital R-string prefixes as those have a - community-accepted meaning (#1244) - -- `Black` now uses exit code 2 when specified configuration file doesn't exit (#1361) - -- `Black` now works on AWS Lambda (#1141) - -- added `--force-exclude` argument (#1032) - -- removed deprecated `--py36` option (#1236) - -- fixed `--diff` output when EOF is encountered (#526) - -- fixed `# fmt: off` handling around decorators (#560) - -- fixed unstable formatting with some `# type: ignore` comments (#1113) - -- fixed invalid removal on organizing brackets followed by indexing (#1575) - -- introduced `black-primer`, a CI tool that allows us to run regression tests against - existing open source users of Black (#1402) - -- introduced property-based fuzzing to our test suite based on Hypothesis and - Hypothersmith (#1566) - -- implemented experimental and disabled by default long string rewrapping (#1132), - hidden under a `--experimental-string-processing` flag while it's being worked on; - this is an undocumented and unsupported feature, you lose Internet points for - depending on it (#1609) - -#### Vim plugin - -- prefer virtualenv packages over global packages (#1383) - -### 19.10b0 - -- added support for PEP 572 assignment expressions (#711) - -- added support for PEP 570 positional-only arguments (#943) - -- added support for async generators (#593) - -- added support for pre-splitting collections by putting an explicit trailing comma - inside (#826) - -- added `black -c` as a way to format code passed from the command line (#761) - -- --safe now works with Python 2 code (#840) - -- fixed grammar selection for Python 2-specific code (#765) - -- fixed feature detection for trailing commas in function definitions and call sites - (#763) - -- `# fmt: off`/`# fmt: on` comment pairs placed multiple times within the same block of - code now behave correctly (#1005) - -- _Black_ no longer crashes on Windows machines with more than 61 cores (#838) - -- _Black_ no longer crashes on standalone comments prepended with a backslash (#767) - -- _Black_ no longer crashes on `from` ... `import` blocks with comments (#829) - -- _Black_ no longer crashes on Python 3.7 on some platform configurations (#494) - -- _Black_ no longer fails on comments in from-imports (#671) - -- _Black_ no longer fails when the file starts with a backslash (#922) - -- _Black_ no longer merges regular comments with type comments (#1027) - -- _Black_ no longer splits long lines that contain type comments (#997) - -- removed unnecessary parentheses around `yield` expressions (#834) - -- added parentheses around long tuples in unpacking assignments (#832) - -- added parentheses around complex powers when they are prefixed by a unary operator - (#646) - -- fixed bug that led _Black_ format some code with a line length target of 1 (#762) - -- _Black_ no longer introduces quotes in f-string subexpressions on string boundaries - (#863) - -- if _Black_ puts parenthesis around a single expression, it moves comments to the - wrapped expression instead of after the brackets (#872) - -- `blackd` now returns the version of _Black_ in the response headers (#1013) - -- `blackd` can now output the diff of formats on source code when the `X-Diff` header is - provided (#969) - -### 19.3b0 - -- new option `--target-version` to control which Python versions _Black_-formatted code - should target (#618) - -- deprecated `--py36` (use `--target-version=py36` instead) (#724) - -- _Black_ no longer normalizes numeric literals to include `_` separators (#696) - -- long `del` statements are now split into multiple lines (#698) - -- type comments are no longer mangled in function signatures - -- improved performance of formatting deeply nested data structures (#509) - -- _Black_ now properly formats multiple files in parallel on Windows (#632) - -- _Black_ now creates cache files atomically which allows it to be used in parallel - pipelines (like `xargs -P8`) (#673) - -- _Black_ now correctly indents comments in files that were previously formatted with - tabs (#262) - -- `blackd` now supports CORS (#622) - -### 18.9b0 - -- numeric literals are now formatted by _Black_ (#452, #461, #464, #469): - - - numeric literals are normalized to include `_` separators on Python 3.6+ code - - - added `--skip-numeric-underscore-normalization` to disable the above behavior and - leave numeric underscores as they were in the input - - - code with `_` in numeric literals is recognized as Python 3.6+ - - - most letters in numeric literals are lowercased (e.g., in `1e10`, `0x01`) - - - hexadecimal digits are always uppercased (e.g. `0xBADC0DE`) - -- added `blackd`, see [its documentation](#blackd) for more info (#349) - -- adjacent string literals are now correctly split into multiple lines (#463) - -- trailing comma is now added to single imports that don't fit on a line (#250) - -- cache is now populated when `--check` is successful for a file which speeds up - consecutive checks of properly formatted unmodified files (#448) - -- whitespace at the beginning of the file is now removed (#399) - -- fixed mangling [pweave](http://mpastell.com/pweave/) and - [Spyder IDE](https://www.spyder-ide.org/) special comments (#532) - -- fixed unstable formatting when unpacking big tuples (#267) - -- fixed parsing of `__future__` imports with renames (#389) - -- fixed scope of `# fmt: off` when directly preceding `yield` and other nodes (#385) - -- fixed formatting of lambda expressions with default arguments (#468) - -- fixed `async for` statements: _Black_ no longer breaks them into separate lines (#372) - -- note: the Vim plugin stopped registering `,=` as a default chord as it turned out to - be a bad idea (#415) - -### 18.6b4 - -- hotfix: don't freeze when multiple comments directly precede `# fmt: off` (#371) - -### 18.6b3 - -- typing stub files (`.pyi`) now have blank lines added after constants (#340) - -- `# fmt: off` and `# fmt: on` are now much more dependable: - - - they now work also within bracket pairs (#329) - - - they now correctly work across function/class boundaries (#335) - - - they now work when an indentation block starts with empty lines or misaligned - comments (#334) - -- made Click not fail on invalid environments; note that Click is right but the - likelihood we'll need to access non-ASCII file paths when dealing with Python source - code is low (#277) - -- fixed improper formatting of f-strings with quotes inside interpolated expressions - (#322) - -- fixed unnecessary slowdown when long list literals where found in a file - -- fixed unnecessary slowdown on AST nodes with very many siblings - -- fixed cannibalizing backslashes during string normalization - -- fixed a crash due to symbolic links pointing outside of the project directory (#338) - -### 18.6b2 - -- added `--config` (#65) - -- added `-h` equivalent to `--help` (#316) - -- fixed improper unmodified file caching when `-S` was used - -- fixed extra space in string unpacking (#305) - -- fixed formatting of empty triple quoted strings (#313) - -- fixed unnecessary slowdown in comment placement calculation on lines without comments - -### 18.6b1 - -- hotfix: don't output human-facing information on stdout (#299) - -- hotfix: don't output cake emoji on non-zero return code (#300) - -### 18.6b0 - -- added `--include` and `--exclude` (#270) - -- added `--skip-string-normalization` (#118) - -- added `--verbose` (#283) - -- the header output in `--diff` now actually conforms to the unified diff spec - -- fixed long trivial assignments being wrapped in unnecessary parentheses (#273) - -- fixed unnecessary parentheses when a line contained multiline strings (#232) - -- fixed stdin handling not working correctly if an old version of Click was used (#276) - -- _Black_ now preserves line endings when formatting a file in place (#258) - -### 18.5b1 - -- added `--pyi` (#249) - -- added `--py36` (#249) - -- Python grammar pickle caches are stored with the formatting caches, making _Black_ - work in environments where site-packages is not user-writable (#192) - -- _Black_ now enforces a PEP 257 empty line after a class-level docstring (and/or - fields) and the first method - -- fixed invalid code produced when standalone comments were present in a trailer that - was omitted from line splitting on a large expression (#237) - -- fixed optional parentheses being removed within `# fmt: off` sections (#224) - -- fixed invalid code produced when stars in very long imports were incorrectly wrapped - in optional parentheses (#234) - -- fixed unstable formatting when inline comments were moved around in a trailer that was - omitted from line splitting on a large expression (#238) - -- fixed extra empty line between a class declaration and the first method if no class - docstring or fields are present (#219) - -- fixed extra empty line between a function signature and an inner function or inner - class (#196) - -### 18.5b0 - -- call chains are now formatted according to the - [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67) - -- data structure literals (tuples, lists, dictionaries, and sets) are now also always - exploded like imports when they don't fit in a single line (#152) - -- slices are now formatted according to PEP 8 (#178) - -- parentheses are now also managed automatically on the right-hand side of assignments - and return statements (#140) - -- math operators now use their respective priorities for delimiting multiline - expressions (#148) - -- optional parentheses are now omitted on expressions that start or end with a bracket - and only contain a single operator (#177) - -- empty parentheses in a class definition are now removed (#145, #180) - -- string prefixes are now standardized to lowercase and `u` is removed on Python 3.6+ - only code and Python 2.7+ code with the `unicode_literals` future import (#188, #198, - #199) - -- typing stub files (`.pyi`) are now formatted in a style that is consistent with PEP - 484 (#207, #210) - -- progress when reformatting many files is now reported incrementally - -- fixed trailers (content with brackets) being unnecessarily exploded into their own - lines after a dedented closing bracket (#119) - -- fixed an invalid trailing comma sometimes left in imports (#185) - -- fixed non-deterministic formatting when multiple pairs of removable parentheses were - used (#183) - -- fixed multiline strings being unnecessarily wrapped in optional parentheses in long - assignments (#215) - -- fixed not splitting long from-imports with only a single name - -- fixed Python 3.6+ file discovery by also looking at function calls with unpacking. - This fixed non-deterministic formatting if trailing commas where used both in function - signatures with stars and function calls with stars but the former would be - reformatted to a single line. - -- fixed crash on dealing with optional parentheses (#193) - -- fixed "is", "is not", "in", and "not in" not considered operators for splitting - purposes - -- fixed crash when dead symlinks where encountered - -### 18.4a4 - -- don't populate the cache on `--check` (#175) - -### 18.4a3 - -- added a "cache"; files already reformatted that haven't changed on disk won't be - reformatted again (#109) - -- `--check` and `--diff` are no longer mutually exclusive (#149) - -- generalized star expression handling, including double stars; this fixes - multiplication making expressions "unsafe" for trailing commas (#132) - -- _Black_ no longer enforces putting empty lines behind control flow statements (#90) - -- _Black_ now splits imports like "Mode 3 + trailing comma" of isort (#127) - -- fixed comment indentation when a standalone comment closes a block (#16, #32) - -- fixed standalone comments receiving extra empty lines if immediately preceding a - class, def, or decorator (#56, #154) - -- fixed `--diff` not showing entire path (#130) - -- fixed parsing of complex expressions after star and double stars in function calls - (#2) - -- fixed invalid splitting on comma in lambda arguments (#133) - -- fixed missing splits of ternary expressions (#141) - -### 18.4a2 - -- fixed parsing of unaligned standalone comments (#99, #112) - -- fixed placement of dictionary unpacking inside dictionary literals (#111) - -- Vim plugin now works on Windows, too - -- fixed unstable formatting when encountering unnecessarily escaped quotes in a string - (#120) - -### 18.4a1 - -- added `--quiet` (#78) - -- added automatic parentheses management (#4) - -- added [pre-commit](https://pre-commit.com) integration (#103, #104) - -- fixed reporting on `--check` with multiple files (#101, #102) - -- fixed removing backslash escapes from raw strings (#100, #105) - -### 18.4a0 - -- added `--diff` (#87) - -- add line breaks before all delimiters, except in cases like commas, to better comply - with PEP 8 (#73) - -- standardize string literals to use double quotes (almost) everywhere (#75) - -- fixed handling of standalone comments within nested bracketed expressions; _Black_ - will no longer produce super long lines or put all standalone comments at the end of - the expression (#22) - -- fixed 18.3a4 regression: don't crash and burn on empty lines with trailing whitespace - (#80) - -- fixed 18.3a4 regression: `# yapf: disable` usage as trailing comment would cause - _Black_ to not emit the rest of the file (#95) - -- when CTRL+C is pressed while formatting many files, _Black_ no longer freaks out with - a flurry of asyncio-related exceptions - -- only allow up to two empty lines on module level and only single empty lines within - functions (#74) - -### 18.3a4 - -- `# fmt: off` and `# fmt: on` are implemented (#5) - -- automatic detection of deprecated Python 2 forms of print statements and exec - statements in the formatted file (#49) - -- use proper spaces for complex expressions in default values of typed function - arguments (#60) - -- only return exit code 1 when --check is used (#50) - -- don't remove single trailing commas from square bracket indexing (#59) - -- don't omit whitespace if the previous factor leaf wasn't a math operator (#55) - -- omit extra space in kwarg unpacking if it's the first argument (#46) - -- omit extra space in - [Sphinx auto-attribute comments](http://www.sphinx-doc.org/en/stable/ext/autodoc.html#directive-autoattribute) - (#68) - -### 18.3a3 - -- don't remove single empty lines outside of bracketed expressions (#19) - -- added ability to pipe formatting from stdin to stdin (#25) - -- restored ability to format code with legacy usage of `async` as a name (#20, #42) - -- even better handling of numpy-style array indexing (#33, again) - -### 18.3a2 - -- changed positioning of binary operators to occur at beginning of lines instead of at - the end, following - [a recent change to PEP 8](https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b) - (#21) - -- ignore empty bracket pairs while splitting. This avoids very weirdly looking - formattings (#34, #35) - -- remove a trailing comma if there is a single argument to a call - -- if top level functions were separated by a comment, don't put four empty lines after - the upper function - -- fixed unstable formatting of newlines with imports - -- fixed unintentional folding of post scriptum standalone comments into last statement - if it was a simple statement (#18, #28) - -- fixed missing space in numpy-style array indexing (#33) - -- fixed spurious space after star-based unary expressions (#31) - -### 18.3a1 - -- added `--check` - -- only put trailing commas in function signatures and calls if it's safe to do so. If - the file is Python 3.6+ it's always safe, otherwise only safe if there are no `*args` - or `**kwargs` used in the signature or call. (#8) - -- fixed invalid spacing of dots in relative imports (#6, #13) - -- fixed invalid splitting after comma on unpacked variables in for-loops (#23) - -- fixed spurious space in parenthesized set expressions (#7) - -- fixed spurious space after opening parentheses and in default arguments (#14, #17) - -- fixed spurious space after unary operators when the operand was a complex expression - (#15) - -### 18.3a0 - -- first published version, Happy 🍰 Day 2018! - -- alpha quality - -- date-versioned (see: https://calver.org/) diff --git a/docs/change_log.md b/docs/change_log.md new file mode 120000 index 00000000000..cf547089dc1 --- /dev/null +++ b/docs/change_log.md @@ -0,0 +1 @@ +../CHANGES.md \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 7381c9d6423..ec6aad9a27b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -219,7 +219,6 @@ def process_sections( DocSection("blackd", CURRENT_DIR / "blackd.md"), DocSection("black_primer", CURRENT_DIR / "black_primer.md"), DocSection("contributing_to_black", CURRENT_DIR / ".." / "CONTRIBUTING.md"), - DocSection("change_log", CURRENT_DIR / ".." / "CHANGES.md"), ] # Sphinx complains when there is a source file that isn't referenced in any of the docs. @@ -231,6 +230,7 @@ def process_sections( "pragmatism", "testimonials", "used_by", + "change_log", } make_pypi_svg(release) From 557b54aa60e9abbb93e5737512d9963e640f2d1f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 26 Apr 2021 10:42:16 -0700 Subject: [PATCH 197/680] Fix crash on docstrings ending with "\ " (#2142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- CHANGES.md | 2 ++ src/black/__init__.py | 8 +++++++- tests/data/docstring.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9fe6b7e3fa4..814956c80ba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ #### _Black_ +- Fix crash on docstrings ending with "\ ". (#2142) + - Reflect the `--skip-magic-trailing-comma` and `--experimental-string-processing` flags in the name of the cache file. Without this fix, changes in these flags would not take effect if the cache had already been populated. (#2131) diff --git a/src/black/__init__.py b/src/black/__init__.py index 99afc7d3c50..051de5d13df 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -2186,7 +2186,13 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if docstring[0] == quote_char: docstring = " " + docstring if docstring[-1] == quote_char: - docstring = docstring + " " + docstring += " " + if docstring[-1] == "\\": + backslash_count = len(docstring) - len(docstring.rstrip("\\")) + if backslash_count % 2: + # Odd number of tailing backslashes, add some padding to + # avoid escaping the closing string quote. + docstring += " " else: # Add some padding if the docstring is empty. docstring = " " diff --git a/tests/data/docstring.py b/tests/data/docstring.py index 9e1c2441e0e..ee6d0c051d7 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -158,6 +158,22 @@ def docstring_with_inline_tabs_and_tab_indentation(): """ pass + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + # output class MyClass: @@ -316,3 +332,18 @@ def docstring_with_inline_tabs_and_tab_indentation(): line ends with some tabs """ pass + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ From eaa337f176b086a9ebd91884c0b9d9a96772aeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 26 Apr 2021 20:24:06 +0200 Subject: [PATCH 198/680] Add more tests for fancy whitespace (#2147) --- CHANGES.md | 2 + tests/data/docstring.py | 28 +++++++++++++ .../data/docstring_no_string_normalization.py | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 814956c80ba..a3bf5565967 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ - Fix crash on docstrings ending with "\ ". (#2142) +- Fix crash when atypical whitespace is cleaned out of dostrings (#2120) + - Reflect the `--skip-magic-trailing-comma` and `--experimental-string-processing` flags in the name of the cache file. Without this fix, changes in these flags would not take effect if the cache had already been populated. (#2131) diff --git a/tests/data/docstring.py b/tests/data/docstring.py index ee6d0c051d7..e977619e482 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -174,6 +174,20 @@ def multiline_backslash_2(): hey there \ ''' +def multiline_backslash_3(): + ''' + already escaped \\ ''' + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave " + + # output class MyClass: @@ -347,3 +361,17 @@ def multiline_backslash_1(): def multiline_backslash_2(): """ hey there \ """ + + +def multiline_backslash_3(): + """ + already escaped \\""" + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave" \ No newline at end of file diff --git a/tests/data/docstring_no_string_normalization.py b/tests/data/docstring_no_string_normalization.py index 0457fcf114f..a90b578f09a 100644 --- a/tests/data/docstring_no_string_normalization.py +++ b/tests/data/docstring_no_string_normalization.py @@ -102,6 +102,26 @@ def shockingly_the_quotes_are_normalized_v2(): ''' pass + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' + # output class ALonelyClass: @@ -207,3 +227,23 @@ def shockingly_the_quotes_are_normalized_v2(): Docstring Docstring Docstring ''' pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' From a0b4e609a7393fac11ecc94eb859c1e85b540036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 26 Apr 2021 21:22:25 +0200 Subject: [PATCH 199/680] Bump pathspec to >= 0.8.1 to solve invalid .gitignore exclusion handling (#2084) Also made the Click requirement in Pipfile consistent with setup.py and bumped mypy. --- Pipfile | 6 +- Pipfile.lock | 697 +++++++++++++++++++++++++-------------------------- setup.py | 2 +- 3 files changed, 344 insertions(+), 361 deletions(-) diff --git a/Pipfile b/Pipfile index 32fb867415f..40f3d607653 100644 --- a/Pipfile +++ b/Pipfile @@ -9,7 +9,7 @@ coverage = "*" docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 flake8 = "*" flake8-bugbear = "*" -mypy = ">=0.782" +mypy = ">=0.812" pre-commit = "*" readme_renderer = "*" recommonmark = "*" @@ -23,9 +23,9 @@ black = {editable = true, extras = ["d"], path = "."} aiohttp = ">=3.3.2" aiohttp-cors = "*" appdirs = "*" -click = ">=7.0" +click = ">=7.1.2" mypy_extensions = ">=0.4.3" -pathspec = ">=0.6" +pathspec = ">=0.8.1" regex = ">=2020.1.8" toml = ">=0.10.1" typed-ast = "==1.4.2" diff --git a/Pipfile.lock b/Pipfile.lock index 3d908dc90f3..8a16a75026a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,46 +16,46 @@ "default": { "aiohttp": { "hashes": [ - "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", - "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", - "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", - "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", - "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", - "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", - "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", - "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", - "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", - "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", - "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", - "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", - "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", - "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", - "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", - "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", - "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", - "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", - "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", - "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", - "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", - "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", - "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", - "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", - "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", - "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", - "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", - "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", - "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", - "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", - "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", - "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", - "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", - "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", - "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", - "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", - "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" + "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" ], "index": "pypi", - "version": "==3.7.4" + "version": "==3.7.4.post0" }, "aiohttp-cors": { "hashes": [ @@ -78,6 +78,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -85,6 +86,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "black": { @@ -96,10 +98,11 @@ }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "click": { "hashes": [ @@ -115,13 +118,16 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "version": "==0.6" + "python_version <": "3.7", + "version": "==0.6", + "version >": "0.6" }, "idna": { "hashes": [ "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], + "markers": "python_version >= '3.4'", "version": "==3.1" }, "multidict": { @@ -164,6 +170,7 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], + "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy-extensions": { @@ -184,50 +191,58 @@ }, "regex": { "hashes": [ - "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", - "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", - "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", - "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", - "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", - "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", - "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", - "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", - "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", - "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", - "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", - "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", - "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", - "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", - "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", - "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", - "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", - "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", - "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", - "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", - "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", - "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", - "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", - "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", - "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", - "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", - "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", - "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", - "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", - "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", - "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", - "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", - "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", - "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", - "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", - "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", - "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", - "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", - "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", - "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", - "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", + "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", + "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", + "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", + "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", + "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", + "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", + "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", + "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", + "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", + "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", + "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", + "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", + "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", + "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", + "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", + "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", + "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", + "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", + "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", + "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", + "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", + "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", + "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", + "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", + "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", + "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", + "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", + "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", + "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", + "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", + "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", + "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", + "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", + "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", + "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", + "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", + "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", + "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", + "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", + "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" ], "index": "pypi", - "version": "==2020.11.13" + "version": "==2021.4.4" + }, + "setuptools-scm": { + "hashes": [ + "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", + "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" }, "toml": { "hashes": [ @@ -280,7 +295,9 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.3" + "python_version <": "3.8", + "version": "==3.7.4.3", + "version >=": "3.7.4" }, "yarl": { "hashes": [ @@ -322,52 +339,53 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], + "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, "develop": { "aiohttp": { "hashes": [ - "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", - "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", - "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", - "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", - "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", - "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", - "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", - "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", - "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", - "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", - "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", - "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", - "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", - "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", - "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", - "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", - "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", - "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", - "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", - "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", - "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", - "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", - "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", - "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", - "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", - "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", - "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", - "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", - "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", - "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", - "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", - "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", - "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", - "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", - "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", - "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", - "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" + "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" ], "index": "pypi", - "version": "==3.7.4" + "version": "==3.7.4.post0" }, "aiohttp-cors": { "hashes": [ @@ -397,6 +415,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -404,6 +423,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "babel": { @@ -411,6 +431,7 @@ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "black": { @@ -425,6 +446,7 @@ "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.3.0" }, "certifi": { @@ -434,61 +456,21 @@ ], "version": "==2020.12.5" }, - "cffi": { - "hashes": [ - "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", - "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", - "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", - "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", - "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", - "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", - "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", - "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", - "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", - "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", - "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", - "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", - "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", - "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", - "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", - "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", - "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", - "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", - "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", - "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", - "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", - "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", - "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", - "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", - "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", - "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", - "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", - "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", - "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", - "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", - "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", - "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", - "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", - "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", - "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", - "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", - "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" - ], - "version": "==1.14.5" - }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "click": { "hashes": [ @@ -503,6 +485,7 @@ "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" }, "commonmark": { @@ -514,75 +497,61 @@ }, "coverage": { "hashes": [ - "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", - "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", - "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", - "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", - "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", - "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", - "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", - "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", - "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", - "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", - "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", - "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", - "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", - "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", - "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", - "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", - "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", - "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", - "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", - "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", - "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", - "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", - "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", - "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", - "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", - "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", - "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", - "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", - "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", - "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", - "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", - "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", - "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", - "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", - "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", - "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", - "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", - "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", - "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", - "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", - "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", - "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", - "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", - "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", - "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", - "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", - "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", - "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", - "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" + "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" ], "index": "pypi", - "version": "==5.3.1" - }, - "cryptography": { - "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" - ], - "version": "==3.4.7" + "version": "==5.5" }, "distlib": { "hashes": [ @@ -594,6 +563,7 @@ "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", + "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -608,32 +578,34 @@ }, "flake8": { "hashes": [ - "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", - "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" + "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", + "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" ], "index": "pypi", - "version": "==3.8.4" + "version": "==3.9.1" }, "flake8-bugbear": { "hashes": [ - "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538", - "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703" + "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7", + "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa" ], "index": "pypi", - "version": "==20.11.1" + "version": "==21.4.3" }, "identify": { "hashes": [ - "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87", - "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034" + "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e", + "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8" ], - "version": "==2.2.2" + "markers": "python_full_version >= '3.6.1'", + "version": "==2.2.4" }, "idna": { "hashes": [ "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], + "markers": "python_version >= '3.4'", "version": "==3.1" }, "imagesize": { @@ -641,28 +613,23 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "importlib-metadata": { "hashes": [ - "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", - "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" - ], - "version": "==3.10.0" - }, - "jeepney": { - "hashes": [ - "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", - "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], - "markers": "sys_platform == 'linux'", - "version": "==0.6.0" + "markers": "python_version >= '3.6'", + "version": "==4.0.1" }, "jinja2": { "hashes": [ "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, "keyring": { @@ -670,6 +637,7 @@ "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" ], + "markers": "python_version >= '3.6'", "version": "==23.0.1" }, "markupsafe": { @@ -727,6 +695,7 @@ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -776,27 +745,36 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], + "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy": { "hashes": [ - "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324", - "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc", - "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802", - "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122", - "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975", - "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7", - "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666", - "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669", - "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178", - "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01", - "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea", - "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de", - "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1", - "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c" + "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", + "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", + "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", + "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", + "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", + "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", + "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", + "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", + "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", + "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", + "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", + "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", + "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", + "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", + "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", + "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", + "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", + "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", + "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", + "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", + "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", + "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" ], "index": "pypi", - "version": "==0.790" + "version": "==0.812" }, "mypy-extensions": { "hashes": [ @@ -808,16 +786,17 @@ }, "nodeenv": { "hashes": [ - "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", - "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" + "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", + "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" ], - "version": "==1.5.0" + "version": "==1.6.0" }, "packaging": { "hashes": [ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pathspec": { @@ -837,38 +816,34 @@ }, "pre-commit": { "hashes": [ - "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", - "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4" + "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712", + "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.12.1" }, "pycodestyle": { "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], - "version": "==2.6.0" - }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.7.0" }, "pyflakes": { "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], - "version": "==2.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.3.1" }, "pygments": { "hashes": [ "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" ], + "markers": "python_version >= '3.5'", "version": "==2.8.1" }, "pyparsing": { @@ -876,6 +851,7 @@ "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" }, "pytz": { @@ -917,15 +893,16 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "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" }, "readme-renderer": { "hashes": [ - "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", - "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" + "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", + "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" ], "index": "pypi", - "version": "==28.0" + "version": "==29.0" }, "recommonmark": { "hashes": [ @@ -937,56 +914,57 @@ }, "regex": { "hashes": [ - "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", - "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", - "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", - "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", - "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", - "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", - "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", - "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", - "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", - "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", - "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", - "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", - "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", - "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", - "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", - "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", - "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", - "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", - "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", - "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", - "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", - "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", - "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", - "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", - "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", - "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", - "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", - "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", - "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", - "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", - "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", - "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", - "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", - "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", - "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", - "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", - "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", - "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", - "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", - "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", - "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", + "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", + "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", + "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", + "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", + "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", + "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", + "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", + "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", + "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", + "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", + "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", + "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", + "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", + "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", + "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", + "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", + "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", + "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", + "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", + "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", + "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", + "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", + "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", + "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", + "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", + "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", + "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", + "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", + "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", + "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", + "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", + "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", + "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", + "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", + "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", + "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", + "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", + "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", + "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", + "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" ], "index": "pypi", - "version": "==2020.11.13" + "version": "==2021.4.4" }, "requests": { "hashes": [ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.25.1" }, "requests-toolbelt": { @@ -1003,27 +981,20 @@ ], "version": "==1.4.0" }, - "secretstorage": { - "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.1" - }, "setuptools-scm": { "hashes": [ - "sha256:62fa535edb31ece9fa65dc9dcb3056145b8020c8c26c0ef1018aef33db95c40d", - "sha256:c85b6b46d0edd40d2301038cdea96bb6adc14d62ef943e75afb08b3e7bcf142a" + "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", + "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" ], - "index": "pypi", - "version": "==5.0.1" + "markers": "python_version >= '3.6'", + "version": "==6.0.1" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -1035,17 +1006,18 @@ }, "sphinx": { "hashes": [ - "sha256:aeef652b14629431c82d3fe994ce39ead65b3fe87cf41b9a3714168ff8b83376", - "sha256:e450cb205ff8924611085183bf1353da26802ae73d9251a8fcdf220a8f8712ef" + "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1", + "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8" ], "index": "pypi", - "version": "==3.4.1" + "version": "==3.5.4" }, "sphinxcontrib-applehelp": { "hashes": [ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -1053,6 +1025,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -1060,6 +1033,7 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -1067,6 +1041,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -1074,6 +1049,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -1081,6 +1057,7 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], + "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { @@ -1096,15 +1073,16 @@ "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.60.0" }, "twine": { "hashes": [ - "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", - "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" + "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83", + "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b" ], "index": "pypi", - "version": "==3.3.0" + "version": "==3.4.1" }, "typed-ast": { "hashes": [ @@ -1149,22 +1127,25 @@ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.3" + "python_version <": "3.8", + "version": "==3.7.4.3", + "version >=": "3.7.4" }, "urllib3": { "hashes": [ "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "index": "pypi", + "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.4" }, "virtualenv": { "hashes": [ - "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107", - "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c" + "sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2", + "sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535" ], - "version": "==20.4.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4.4" }, "webencodings": { "hashes": [ @@ -1221,6 +1202,7 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], + "markers": "python_version >= '3.6'", "version": "==1.6.3" }, "zipp": { @@ -1228,6 +1210,7 @@ "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], + "markers": "python_version >= '3.6'", "version": "==3.4.1" } } diff --git a/setup.py b/setup.py index 856c7fadb0c..0928c63afc2 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_long_description() -> str: "toml>=0.10.1", "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", - "pathspec>=0.6, <1", + "pathspec>=0.8.1, <1", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.7.4; python_version < '3.8'", "mypy_extensions>=0.4.3", From b55ea63ff4858109fe4606a6caafb03fda946743 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 26 Apr 2021 16:26:43 -0400 Subject: [PATCH 200/680] Stop stripping parens in even more illegal spots (#2148) We're only fixing them so fuzzers don't yell at us when we break "valid" code. I mean "valid" because some of the examples aren't even accepted by Python. --- src/black/__init__.py | 3 +++ tests/data/pep_572_do_not_remove_parens.py | 21 +++++++++++++++++++++ tests/data/pep_572_remove_parens.py | 15 +++++++++++++++ tests/test_black.py | 8 ++++++++ 4 files changed, 47 insertions(+) create mode 100644 tests/data/pep_572_do_not_remove_parens.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 051de5d13df..015ca415f1c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5624,6 +5624,9 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: syms.expr_stmt, syms.assert_stmt, syms.return_stmt, + # these ones aren't useful to end users, but they do please fuzzers + syms.for_stmt, + syms.del_stmt, ]: return False diff --git a/tests/data/pep_572_do_not_remove_parens.py b/tests/data/pep_572_do_not_remove_parens.py new file mode 100644 index 00000000000..20e80a69377 --- /dev/null +++ b/tests/data/pep_572_do_not_remove_parens.py @@ -0,0 +1,21 @@ +# 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 + +del (a := [1]) + +try: + pass +except (a := 1) as (b := why_does_this_exist): + pass + +for (z := 124) in (x := -124): + pass + +with (y := [3, 2, 1]) as (funfunfun := indeed): + pass + + +@(please := stop) +def sigh(): + pass diff --git a/tests/data/pep_572_remove_parens.py b/tests/data/pep_572_remove_parens.py index 9a804859a09..9718d95b499 100644 --- a/tests/data/pep_572_remove_parens.py +++ b/tests/data/pep_572_remove_parens.py @@ -33,6 +33,9 @@ def foo2(answer: (p := 42) = 5): lambda: (x := 1) +a[(x := 12)] +a[:(x := 13)] + # we don't touch expressions in f-strings but if we do one day, don't break 'em f'{(x:=10)}' @@ -43,6 +46,10 @@ def a(): yield (a := 2) raise (c := 3) +def this_is_so_dumb() -> (please := no): + pass + + # output if foo := 0: pass @@ -79,6 +86,9 @@ def foo2(answer: (p := 42) = 5): lambda: (x := 1) +a[(x := 12)] +a[:(x := 13)] + # we don't touch expressions in f-strings but if we do one day, don't break 'em f"{(x:=10)}" @@ -88,3 +98,8 @@ def a(): await (b := 1) yield (a := 2) raise (c := 3) + + +def this_is_so_dumb() -> (please := no): + pass + diff --git a/tests/test_black.py b/tests/test_black.py index 88ea31777fb..c643f27cacf 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -290,6 +290,14 @@ def test_pep_572_remove_parens(self) -> None: 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) From 303a0b2c4d0c828ba6cd4ef6d480f1ff2b5a11c6 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 26 Apr 2021 16:59:45 -0400 Subject: [PATCH 201/680] Maintainers += Richard Si (aka ichard26) (#2149) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed9f105f5aa..8d6adb0567e 100644 --- a/README.md +++ b/README.md @@ -539,8 +539,8 @@ 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), and -[Cooper Lees](mailto:me@cooperlees.com). +[Zsolt Dollenstein](mailto:zsol.zsol@gmail.com), +[Cooper Lees](mailto:me@cooperlees.com), and Richard Si. Multiple contributions by: From 1728bb441e6cc3ba5a5ce52ec62014bbfa435a82 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 27 Apr 2021 07:40:08 -0700 Subject: [PATCH 202/680] Update CHANGELOG for 21.4b1 release (#2151) * Update CHANGELOG for 21.4b1 release * Add pathspec minimum bump + update primer not to expect changes for virtualenv --- CHANGES.md | 6 +++++- src/black_primer/primer.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a3bf5565967..d114c5136ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ ## Change Log -### Unreleased +### 21.4b1 #### _Black_ @@ -15,6 +15,10 @@ - Don't remove necessary parentheses from assignment expression containing assert / return statements. (#2143) +#### _Packaging_ + +- Bump pathspec to >= 0.8.1 to solve invalid .gitignore exclusion handling + ### 21.4b0 #### _Black_ diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 7a6f5abf408..d38e01dd707 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -112,7 +112,7 @@ }, "virtualenv": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/pypa/virtualenv.git", "long_checkout": false, "py_versions": ["all"] From 82a53999ea971895e2836df39be1ec9110ec102c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 27 Apr 2021 08:33:51 -0700 Subject: [PATCH 203/680] Add pyanalyze and typeshed to black-primer (#2152) pyanalyze is one of my projects and it uses `--experimental-string-processing`. typeshed has a lot of stub files. --- src/black_primer/primer.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index d38e01dd707..75469ad815b 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -75,6 +75,13 @@ "long_checkout": false, "py_versions": ["all"] }, + "pyanalyze": { + "cli_arguments": [], + "expect_formatting_changes": false, + "git_clone_url": "https://github.com/quora/pyanalyze.git", + "long_checkout": false, + "py_versions": ["all"] + }, "pyramid": { "cli_arguments": [], "expect_formatting_changes": true, @@ -110,6 +117,13 @@ "long_checkout": false, "py_versions": ["all"] }, + "typeshed": { + "cli_arguments": [], + "expect_formatting_changes": true, + "git_clone_url": "https://github.com/python/typeshed.git", + "long_checkout": false, + "py_versions": ["all"] + }, "virtualenv": { "cli_arguments": [], "expect_formatting_changes": false, From 76e1602d60391cca1bb3b21a9ef6ed07a1bca17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 27 Apr 2021 17:58:39 +0200 Subject: [PATCH 204/680] Install primer.json (used by black-primer by default) with black (#2154) Fixes https://github.com/psf/black/issues/2153 --- CHANGES.md | 6 ++++++ setup.py | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d114c5136ff..37b5da6a40d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ ## Change Log +### Unreleased + +#### _Packaging_ + +- Install `primer.json` (used by `black-primer` by default) with black. (#2154) + ### 21.4b1 #### _Black_ diff --git a/setup.py b/setup.py index 0928c63afc2..f1792a46fe8 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,11 @@ def get_long_description() -> str: ext_modules=ext_modules, packages=["blackd", "black", "blib2to3", "blib2to3.pgen2", "black_primer"], package_dir={"": "src"}, - package_data={"blib2to3": ["*.txt"], "black": ["py.typed"]}, + package_data={ + "blib2to3": ["*.txt"], + "black": ["py.typed"], + "black_primer": ["primer.json"], + }, python_requires=">=3.6.2", zip_safe=False, install_requires=[ From 807a65f9d59d97a606575dc69110d7a5dfd98641 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 27 Apr 2021 14:00:17 -0700 Subject: [PATCH 205/680] Update discussion of AST safety check in README (#2159) --- CHANGES.md | 5 +++++ README.md | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 37b5da6a40d..a30668d33b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,11 @@ ### Unreleased +#### _Black_ + +- Clarify that _Black_ may change the AST, especially when cleaning up docstrings. + (#2159) + #### _Packaging_ - Install `primer.json` (used by `black-primer` by default) with black. (#2154) diff --git a/README.md b/README.md index 8d6adb0567e..c7577fbbd04 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,10 @@ change in the future**. That being said, no drastic stylistic changes are planne mostly responses to bug reports. Also, as a temporary safety measure, _Black_ will check that the reformatted code still -produces a valid AST that is equivalent to the original. This slows it down. If you're -feeling confident, use `--fast`. +produces a valid AST that is mostly equivalent to the original. This slows it down. If +you're feeling confident, use `--fast`. In a few contexts, Black does make changes to +the AST: it cleans up whitespace in docstrings, adds or removes parentheses in some +`del` statements, and may move around type comments. ## The _Black_ code style From ad1696422b7343a0cfea3c4b304e6acfa15d3cff Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 27 Apr 2021 14:16:35 -0700 Subject: [PATCH 206/680] Ignore inaccessible user config (#2158) Fixes #2157 --- CHANGES.md | 2 ++ src/black/__init__.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a30668d33b2..cae61f25b4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ #### _Black_ +- Fix crash if the user configuration directory is inaccessible. (#2158) + - Clarify that _Black_ may change the AST, especially when cleaning up docstrings. (#2159) diff --git a/src/black/__init__.py b/src/black/__init__.py index 015ca415f1c..1c69cc41cdc 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -311,8 +311,17 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: if path_pyproject_toml.is_file(): return str(path_pyproject_toml) - path_user_pyproject_toml = find_user_pyproject_toml() - return str(path_user_pyproject_toml) if path_user_pyproject_toml.is_file() else None + try: + path_user_pyproject_toml = find_user_pyproject_toml() + return ( + str(path_user_pyproject_toml) + if path_user_pyproject_toml.is_file() + else None + ) + except PermissionError 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 def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: From 04fd4432f6c6007e419d7174930569a75ea60fed Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 27 Apr 2021 20:10:44 -0700 Subject: [PATCH 207/680] Add automatic version tagging to Docker CI Pushes (#2132) * Add automatic version tagging to Docker Uploads - If the git comment has a tag, set that on the docker images pushed - If we don't have a tag, we just set `latest_non_release` * Add trigger on release creation too * Make prettier happy omn docker.yml --- .github/workflows/docker.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 09390130b3d..7eaf233ab9c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,6 +4,8 @@ on: push: branches: - "master" + release: + types: created jobs: docker: @@ -24,13 +26,18 @@ jobs: 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@v2 with: context: . platforms: linux/amd64,linux/arm64 push: true - tags: pyfound/black:latest + tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} From 89a856d742d31054bc31a6ff7ffc6ad2d24377d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 28 Apr 2021 16:07:15 +0200 Subject: [PATCH 208/680] Remove useless shebangs in non-executable files (#2161) Such shebangs are only ever used if the file is executed directly, i.e.: $ /usr/lib/python3.9/site-packages/black_primer/cli.py But that doesn't work: $ /usr/lib/python3.9/site-packages/black_primer/cli.py bash: /usr/lib/python3.9/site-packages/black_primer/cli.py: Permission denied The lib file even has: "lib is a library, funnily enough" --- src/black_primer/cli.py | 2 -- src/black_primer/lib.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index 5903adc72d4..b2d4159b1da 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # coding=utf8 import asyncio diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 7999f6a3e36..ecc704f2cca 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import asyncio import errno import json From 682077c15e914fa661879003e186ffc90ac66dcc Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 28 Apr 2021 07:21:56 -0700 Subject: [PATCH 209/680] Update CHANGELOG for 21.4b2 release (#2162) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cae61f25b4e..5b9f574a408 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ ## Change Log -### Unreleased +### 21.4b2 #### _Black_ From b39999da7f451c285befac217f1f9a685774b34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 28 Apr 2021 16:48:04 +0200 Subject: [PATCH 210/680] Elaborate on what AST changes Black might perform --- CHANGES.md | 5 +++-- README.md | 10 +++++----- docs/the_black_code_style.md | 29 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5b9f574a408..6675c484c75 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,8 +6,9 @@ - Fix crash if the user configuration directory is inaccessible. (#2158) -- Clarify that _Black_ may change the AST, especially when cleaning up docstrings. - (#2159) +- Clarify + [circumstances](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#pragmatism) + in which _Black_ may change the AST (#2159) #### _Packaging_ diff --git a/README.md b/README.md index c7577fbbd04..41cea761df5 100644 --- a/README.md +++ b/README.md @@ -237,11 +237,11 @@ is that **until the formatter becomes stable, you should expect some formatting change in the future**. That being said, no drastic stylistic changes are planned, mostly responses to bug reports. -Also, as a temporary safety measure, _Black_ will check that the reformatted code still -produces a valid AST that is mostly equivalent to the original. This slows it down. If -you're feeling confident, use `--fast`. In a few contexts, Black does make changes to -the AST: it cleans up whitespace in docstrings, adds or removes parentheses in some -`del` statements, and may move around type comments. +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](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#pragmatism) +section for details). If you're feeling confident, use `--fast`. ## The _Black_ code style diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 67ccb8aab11..39a452ff9a8 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -464,3 +464,32 @@ exception to this rule is r-strings. It turns out that the very popular 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. + +### 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: + +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 + whitespace anyway. + +2. _Black_ manages optional parentheses for some statements. In the case of the `del` + statement, presence of wrapping parentheses or lack of thereof changes the resulting + AST but is semantically equivalent in the interpreter. + +3. _Black_ might move comments around, which includes type comments. Those are part of + the AST as of Python 3.8. While the tool implements a number of special cases for + those comments, there is no guarantee they will remain where they were in the source. + Note that this doesn't change runtime behavior of the source code. + +To put things in perspective, the code equivalence check is a feature of _Black_ which +other formatters don't implement at all. It is of crucial importance to us to ensure +code behaves the way it did before it got reformatted. We treat this as a feature and +there are no plans to relax this in the future. The exceptions enumerated above stem +from either user feedback or implementation details of the tool. In each case we made +due diligence to ensure that the AST divergence is of no practical consequence. From 00491e1dcb19a795867249471ca5bb95efcd8cd3 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 1 May 2021 13:47:59 -0500 Subject: [PATCH 211/680] Add ability to pass posargs to pytest run in tox.ini (#2173) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 71b02e920c7..affc3c9876a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = commands = pip install -e .[d,python2] coverage erase - coverage run -m pytest tests + coverage run -m pytest tests {posargs} coverage report [testenv:fuzz] From 24bd6b983ac459eabd9352eb816be53e7f447812 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 May 2021 22:17:20 +0300 Subject: [PATCH 212/680] Tox has been formatted with Black 21.4b0 (#2175) --- 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 75469ad815b..5017a5625a3 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -112,7 +112,7 @@ }, "tox": { "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/tox-dev/tox.git", "long_checkout": false, "py_versions": ["all"] From 35e8d1560d68e8113ff926a9f832582f8f4a694f Mon Sep 17 00:00:00 2001 From: Bryan Forbes Date: Sun, 2 May 2021 07:48:54 -0500 Subject: [PATCH 213/680] Set `is_pyi` if `stdin_filename` ends with `.pyi` (#2169) Fixes #2167 Co-authored-by: Jelle Zijlstra --- CHANGES.md | 6 ++++++ src/black/__init__.py | 2 ++ tests/test_black.py | 32 +++++++++++++++++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6675c484c75..321f8c083b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ ## Change Log +### Unreleased + +#### _Black_ + +- Set `--pyi` mode if `--stdin-filename` ends in `.pyi` (#2169) + ### 21.4b2 #### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 1c69cc41cdc..49d088b531d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -755,6 +755,8 @@ def reformat_one( is_stdin = False if is_stdin: + if src.suffix == ".pyi": + mode = replace(mode, is_pyi=True) if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode): changed = Changed.YES else: diff --git a/tests/test_black.py b/tests/test_black.py index c643f27cacf..43368d4bbe9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1578,8 +1578,34 @@ def test_reformat_one_with_stdin_filename(self) -> None: mode=DEFAULT_MODE, report=report, ) - fsts.assert_called_once() - # __BLACK_STDIN_FILENAME__ should have been striped + fsts.assert_called_once_with( + fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE + ) + # __BLACK_STDIN_FILENAME__ should have been stripped + report.done.assert_called_with(expected, black.Changed.YES) + + def test_reformat_one_with_stdin_filename_pyi(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + p = "foo.pyi" + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once_with( + fast=True, + write_back=black.WriteBack.YES, + mode=replace(DEFAULT_MODE, is_pyi=True), + ) + # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) def test_reformat_one_with_stdin_and_existing_path(self) -> None: @@ -1603,7 +1629,7 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None: report=report, ) fsts.assert_called_once() - # __BLACK_STDIN_FILENAME__ should have been striped + # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) def test_gitignore_exclude(self) -> None: From a669b64091c149132f4e234abd230f7b33d692e8 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 3 May 2021 14:58:17 -0700 Subject: [PATCH 214/680] primer: Renable pandas (#2185) - It no longer crashes black so we should test on it's code - Update django reason to name the file causing error - Seems it has a syntax error on purpose --- src/black_primer/primer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 5017a5625a3..137d075c68e 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -30,7 +30,7 @@ "py_versions": ["all"] }, "django": { - "disabled_reason": "black --check --diff returned 123 on two files", + "disabled_reason": "black --check --diff returned 123 on tests_syntax_error.py", "disabled": true, "cli_arguments": [], "expect_formatting_changes": true, @@ -53,8 +53,6 @@ "py_versions": ["all"] }, "pandas": { - "disabled_reason": "black --check --diff returned 123 on one file", - "disabled": true, "cli_arguments": [], "expect_formatting_changes": true, "git_clone_url": "https://github.com/pandas-dev/pandas.git", From a18c7bc09942951e93cbd142fb7384aa2af18951 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 4 May 2021 01:44:40 -0700 Subject: [PATCH 215/680] primer: Add `--no-diff` option (#2187) - Allow runs with no code diff output - This is handy for reducing output to see which file is erroring Test: - Edit config for 'channels' to expect no changes and run with `--no-diff` and see no diff output --- CHANGES.md | 1 + src/black_primer/cli.py | 15 ++++++++++++++- src/black_primer/lib.py | 25 +++++++++++++++++++++---- tests/test_primer.py | 1 + 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 321f8c083b2..7741d92772d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ #### _Black_ - Set `--pyi` mode if `--stdin-filename` ends in `.pyi` (#2169) +- Add `--no-diff` to black-primer to suppress formatting changes (#2187) ### 21.4b2 diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index b2d4159b1da..00b54d6511f 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -39,6 +39,7 @@ async def async_main( debug: bool, keep: bool, long_checkouts: bool, + no_diff: bool, rebase: bool, workdir: str, workers: int, @@ -54,7 +55,13 @@ async def async_main( try: ret_val = await lib.process_queue( - config, work_path, workers, keep, long_checkouts, rebase + config, + work_path, + workers, + keep, + long_checkouts, + rebase, + no_diff, ) return int(ret_val) finally: @@ -95,6 +102,12 @@ async def async_main( 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( "-R", "--rebase", diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index ecc704f2cca..aba694b0e60 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -112,13 +112,20 @@ def analyze_results(project_count: int, results: Results) -> int: async def black_run( - repo_path: Path, project_config: Dict[str, Any], results: Results + repo_path: Path, + project_config: Dict[str, Any], + results: Results, + no_diff: bool = False, ) -> None: """Run Black and record failures""" cmd = [str(which(BLACK_BINARY))] if "cli_arguments" in project_config and project_config["cli_arguments"]: cmd.extend(*project_config["cli_arguments"]) - cmd.extend(["--check", "--diff", "."]) + cmd.append("--check") + if no_diff: + cmd.append(".") + else: + cmd.extend(["--diff", "."]) with TemporaryDirectory() as tmp_path: # Prevent reading top-level user configs by manipulating envionment variables @@ -246,6 +253,7 @@ async def project_runner( 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() @@ -284,7 +292,7 @@ async def project_runner( repo_path = await git_checkout_or_rebase(work_path, project_config, rebase) if not repo_path: continue - await black_run(repo_path, project_config, results) + await black_run(repo_path, project_config, results, no_diff) if not keep: LOG.debug(f"Removing {repo_path}") @@ -303,6 +311,7 @@ async def process_queue( keep: bool = False, long_checkouts: bool = False, rebase: bool = False, + no_diff: bool = False, ) -> int: """ Process the queue with X workers and evaluate results @@ -330,7 +339,15 @@ async def process_queue( await asyncio.gather( *[ project_runner( - i, config, queue, work_path, results, long_checkouts, rebase, keep + i, + config, + queue, + work_path, + results, + long_checkouts, + rebase, + keep, + no_diff, ) for i in range(workers) ] diff --git a/tests/test_primer.py b/tests/test_primer.py index a8ad8a7c5af..8bfecd61a57 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -198,6 +198,7 @@ def test_async_main(self) -> None: "rebase": False, "workdir": str(work_dir), "workers": 69, + "no_diff": False, } with patch("black_primer.cli.lib.process_queue", return_zero): return_val = loop.run_until_complete(cli.async_main(**args)) From e42f9921e291790202147c1e3dc1a3df7036c652 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 4 May 2021 04:46:46 -0400 Subject: [PATCH 216/680] Detect `'@' dotted_name '(' ')' NEWLINE` as a simple decorator (#2182) Previously the RELAXED_DECORATOR detection would be falsely True on that example. The problem was that an argument-less parentheses pair didn't pass the `is_simple_decorator_trailer` check even it should. OTOH a parentheses pair containing an argument or more passed as expected. --- CHANGES.md | 3 +++ src/black/__init__.py | 7 +++++++ tests/data/decorators.py | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7741d92772d..00d6782dc6b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,9 @@ - Set `--pyi` mode if `--stdin-filename` ends in `.pyi` (#2169) - Add `--no-diff` to black-primer to suppress formatting changes (#2187) +- Stop detecting target version as Python 3.9+ with pre-PEP-614 decorators that are + being called but with no arguments (#2182) + ### 21.4b2 #### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 49d088b531d..cf257876c03 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -5761,6 +5761,13 @@ def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: and node.children[0].type == token.DOT and node.children[1].type == token.NAME ) + # last trailer can be an argument-less parentheses pair + or ( + last + and len(node.children) == 2 + and node.children[0].type == token.LPAR + and node.children[1].type == token.RPAR + ) # last trailer can be arguments or ( last diff --git a/tests/data/decorators.py b/tests/data/decorators.py index acfad51fcb8..a0f38ca7b9d 100644 --- a/tests/data/decorators.py +++ b/tests/data/decorators.py @@ -13,6 +13,12 @@ def f(): ## +@decorator() +def f(): + ... + +## + @decorator(arg) def f(): ... From 204f76e0c02a12aeccb3ab708fe41f7e95435a29 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> Date: Tue, 4 May 2021 18:47:22 +1000 Subject: [PATCH 217/680] add test configurations that don't contain python2 optional install (#2190) add test for negative scenario: formatting python2 code tag python2 only tests Co-authored-by: KotlinIsland --- pyproject.toml | 3 +++ tests/test_black.py | 30 ++++++++++++++++++++++++++++++ tests/test_format.py | 16 +++++++++++++--- tox.ini | 6 ++++-- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f632f2839d..ca75f8f92ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,3 +25,6 @@ extend-exclude = ''' [build-system] requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +markers = ['python2', "without_python2"] \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 43368d4bbe9..7d855cab3d3 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -24,6 +24,7 @@ Iterator, TypeVar, ) +import pytest import unittest from unittest.mock import patch, MagicMock @@ -459,6 +460,34 @@ def test_skip_magic_trailing_comma(self) -> None: ) self.assertEqual(expected, actual, msg) + @pytest.mark.without_python2 + def test_python2_should_fail_without_optional_install(self) -> None: + # python 3.7 and below will install typed-ast and will be able to parse Python 2 + if sys.version_info < (3, 8): + return + source = "x = 1234l" + tmp_file = Path(black.dump_to_file(source)) + try: + runner = BlackRunner() + result = runner.invoke(black.main, [str(tmp_file)]) + self.assertEqual(result.exit_code, 123) + finally: + os.unlink(tmp_file) + actual = ( + runner.stderr_bytes.decode() + .replace("\n", "") + .replace("\\n", "") + .replace("\\r", "") + .replace("\r", "") + ) + msg = ( + "The requested source code has invalid Python 3 syntax." + "If you are trying to format Python 2 files please reinstall Black" + " with the 'python2' extra: `python3 -m pip install black[python2]`." + ) + self.assertIn(msg, actual) + + @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") @@ -1971,6 +2000,7 @@ 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 diff --git a/tests/test_format.py b/tests/test_format.py index e1335aedf43..78f2b558a4f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,6 +1,7 @@ from unittest.mock import patch import black +import pytest from parameterized import parameterized from tests.util import ( @@ -46,9 +47,6 @@ "function2", "function_trailing_comma", "import_spacing", - "numeric_literals_py2", - "python2", - "python2_unicode_literals", "remove_parens", "slices", "string_prefixes", @@ -56,6 +54,12 @@ "tupleassign", ] +SIMPLE_CASES_PY2 = [ + "numeric_literals_py2", + "python2", + "python2_unicode_literals", +] + EXPERIMENTAL_STRING_PROCESSING_CASES = [ "cantfit", "comments7", @@ -86,6 +90,12 @@ 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: diff --git a/tox.ini b/tox.ini index affc3c9876a..cbb0f75d145 100644 --- a/tox.ini +++ b/tox.ini @@ -7,9 +7,11 @@ skip_install = True deps = -r{toxinidir}/test_requirements.txt commands = - pip install -e .[d,python2] + pip install -e .[d] coverage erase - coverage run -m pytest tests {posargs} + coverage run -m pytest tests -m "not python2" {posargs} + pip install -e .[d,python2] + coverage run -m pytest tests -m "not without_python2" {posargs} coverage report [testenv:fuzz] From 5918a016ff82e5fa12097d07b1624a89ec4e60ac Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 4 May 2021 04:47:59 -0400 Subject: [PATCH 218/680] Drop Travis CI and migrate Coveralls (#2186) Travis CI for Open Source is shutting down in a few weeks so the queue for jobs is insane due to lower resources. I'm 99.99% sure we don't need it as our Test, Lint, Docs, Upload / Package, Primer, and Fuzz workflows are all on GitHub Actions. So even though we *can* migrate to the .com version with its 1000 free Linux minutes(?), I don't think we need to. more information here: - https://blog.travis-ci.com/oss-announcement - https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing - https://docs.travis-ci.com/user/migrate/open-source-repository-migration This commit does the following: - delete the Travis CI configuration - add to the GHA test workflows so coverage continues to be recorded - tweaked coverage configuration so this wouldn't break - remove any references to Travis CI in the docs (i.e. readme + sphinx docs) Regarding the Travis CI to GitHub Actions Coveralls transition, the official action doesn't support the coverage files produced by coverage.py unfornately. Also no, I don't really know what I am doing so don't @ me if this breaks :p (well you can, but don't expect me to be THAT useful). The Coveralls setup has two downfalls AFAIK: - Only Linux runs are used because AndreMiras/coveralls-python-action only supports Linux. Although this isn't a big issue since the Travis Coveralls configuration only used Linux data too. - Pull requests from an internal branch (i.e. one on psf/black) will be marked as a push coverage build by Coveralls since our anti-duplicate- workflows system runs under the push even for such cases. --- .coveragerc | 3 +++ .github/workflows/test.yml | 31 +++++++++++++++++++++++++++++++ .travis.yml | 31 ------------------------------- README.md | 1 - docs/conf.py | 1 - 5 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 .travis.yml diff --git a/.coveragerc b/.coveragerc index 32a8da521ba..f05041ca1dc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,6 @@ omit = src/blib2to3/* tests/data/* */site-packages/* + +[run] +relative_files = True diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bec769064c2..03adc7f0bea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,3 +34,34 @@ jobs: - name: Unit tests run: | tox -e py + + - 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) + if: + ((github.event_name == 'push' && github.repository == 'psf/black') || + github.event.pull_request.base.repo.full_name == 'psf/black') && matrix.os == + 'ubuntu-latest' + + uses: AndreMiras/coveralls-python-action@v20201129 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + flag-name: py${{ matrix.python-version }}-${{ matrix.os }} + 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' + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Coveralls finished + uses: AndreMiras/coveralls-python-action@v20201129 + with: + parallel-finished: true + debug: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e782272e813..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: python -cache: - pip: true - directories: - - $HOME/.cache/pre-commit -env: - - TEST_CMD="tox -e py" -install: - - pip install coverage coveralls pre-commit tox - - pip install -e '.[d]' -script: - - $TEST_CMD -after_success: - - coveralls -notifications: - on_success: change - on_failure: always -matrix: - include: - - name: "lint" - python: 3.7 - env: - - TEST_CMD="pre-commit run --all-files --show-diff-on-failure" - - name: "3.6" - python: 3.6 - - name: "3.7" - python: 3.7 - - name: "3.8" - python: 3.8 - - name: "3.9" - python: 3.9 diff --git a/README.md b/README.md index 41cea761df5..696bfa7e064 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@

The Uncompromising Code Formatter

-Build Status Actions Status Actions Status Documentation Status diff --git a/docs/conf.py b/docs/conf.py index ec6aad9a27b..9e03d05e937 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -314,7 +314,6 @@ def process_sections( "show_powered_by": True, "fixed_sidebar": True, "logo": "logo2.png", - "travis_button": True, } From 0c60ccc06646030bd48d4e160c6aae755307c2d9 Mon Sep 17 00:00:00 2001 From: reka <382113+reka@users.noreply.github.com> Date: Tue, 4 May 2021 10:48:59 +0200 Subject: [PATCH 219/680] compatible isort config: mention profile first (#2180) Change the order of possible ways to configure isort: 1. using the profile black 2. custom configuration Formats section: change the examples to use the profile black Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/compatible_configs.md | 50 +++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index a4f83ee472f..de81769f72d 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -19,7 +19,24 @@ Compatible configuration files can be _Black_ also formats imports, but in a different way from isort's defaults which leads to conflicting changes. -### Configuration +### Profile + +Since version 5.0.0, isort supports +[profiles](https://pycqa.github.io/isort/docs/configuration/profiles/) to allow easy +interoperability with common code styles. You can set the black profile in any of the +[config files](https://pycqa.github.io/isort/docs/configuration/config_files/) supported +by isort. Below, an example for `pyproject.toml`: + +```toml +[tool.isort] +profile = "black" +``` + +### Custom Configuration + +If you're using an isort version that is older than 5.0.0 or you have some custom +configuration for _Black_, you can tweak your isort configuration to make it compatible +with _Black_. Below, an example for `.isort.cfg`: ``` multi_line_output = 3 @@ -72,9 +89,6 @@ works the same as with _Black_. **Please note** `ensure_newline_before_comments = True` only works since isort >= 5 but does not break older versions so you can keep it if you are running previous versions. -If only isort >= 5 is used you can add `profile = black` instead of all the options -since [profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/) -are available and do the configuring for you. ### Formats @@ -83,12 +97,7 @@ are available and do the configuring for you. ```cfg [settings] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 +profile = black ``` @@ -98,12 +107,7 @@ line_length = 88 ```cfg [isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 +profile = black ``` @@ -113,12 +117,7 @@ line_length = 88 ```toml [tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -line_length = 88 +profile = 'black' ``` @@ -128,12 +127,7 @@ line_length = 88 ```ini [*.py] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 +profile = black ``` From 267bc5dde9f4e5e4b6dacdf79cf1688ffe9b7715 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 4 May 2021 14:41:04 +0300 Subject: [PATCH 220/680] Use pre-commit/action to simplify CI (#2191) --- .github/workflows/lint.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2480a5ec131..51f6d02e2e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,8 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade pre-commit python -m pip install -e '.[d]' - name: Lint - run: pre-commit run --all-files --show-diff-on-failure + uses: pre-commit/action@v2.0.2 From d8a034f9b69b3a214e01985e741c4418ae562e2e Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 4 May 2021 11:07:08 -0700 Subject: [PATCH 221/680] Update CHANGES.md for 21.5b0 release (#2192) * Update CHANGES.md for 21.5b0 release * Make prettier happy --- CHANGES.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 00d6782dc6b..becf69904fb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,17 @@ ## Change Log -### Unreleased +### 21.5b0 #### _Black_ - Set `--pyi` mode if `--stdin-filename` ends in `.pyi` (#2169) -- Add `--no-diff` to black-primer to suppress formatting changes (#2187) - - Stop detecting target version as Python 3.9+ with pre-PEP-614 decorators that are being called but with no arguments (#2182) +#### _Black-Primer_ + +- Add `--no-diff` to black-primer to suppress formatting changes (#2187) + ### 21.4b2 #### _Black_ From 14c76e89716b5b53c97ece80bb935ea956b7dd89 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 4 May 2021 12:49:20 -0700 Subject: [PATCH 222/680] Disable pandas while we look into #2193 (#2195) --- src/black_primer/primer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 137d075c68e..78c1e2acdef 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -53,6 +53,8 @@ "py_versions": ["all"] }, "pandas": { + "disabled_reason": "black-primer runs failing on Pandas - #2193", + "disabled": true, "cli_arguments": [], "expect_formatting_changes": true, "git_clone_url": "https://github.com/pandas-dev/pandas.git", From 07c8812937cf75ac5bc7ceac07ef5ea383f10f2f Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 5 May 2021 08:33:23 -0700 Subject: [PATCH 223/680] Enable ` --experimental-string-processing` on most primer projects (#2184) * Enable ` --experimental-string-processing` on all primer projects - We want to make this default so need to test it more - Fixed splat/star bug in extending black args for each project * Disable sqlalchemy due to crash --- src/black_primer/lib.py | 2 +- src/black_primer/primer.json | 47 ++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index aba694b0e60..3ce383f17ce 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -120,7 +120,7 @@ async def black_run( """Run Black and record failures""" cmd = [str(which(BLACK_BINARY))] if "cli_arguments" in project_config and project_config["cli_arguments"]: - cmd.extend(*project_config["cli_arguments"]) + cmd.extend(project_config["cli_arguments"]) cmd.append("--check") if no_diff: cmd.append(".") diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 78c1e2acdef..76ed4820487 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -2,28 +2,28 @@ "configuration_format_version": 20200509, "projects": { "aioexabgp": { - "cli_arguments": [], + "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": [], + "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": [], - "expect_formatting_changes": false, + "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": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/django/channels.git", "long_checkout": false, @@ -32,22 +32,22 @@ "django": { "disabled_reason": "black --check --diff returned 123 on tests_syntax_error.py", "disabled": true, - "cli_arguments": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/django/django.git", "long_checkout": false, "py_versions": ["all"] }, "flake8-bugbear": { - "cli_arguments": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": false, "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git", "long_checkout": false, "py_versions": ["all"] }, "hypothesis": { - "cli_arguments": [], - "expect_formatting_changes": false, + "cli_arguments": ["--experimental-string-processing"], + "expect_formatting_changes": true, "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git", "long_checkout": false, "py_versions": ["all"] @@ -55,55 +55,56 @@ "pandas": { "disabled_reason": "black-primer runs failing on Pandas - #2193", "disabled": true, - "cli_arguments": [], + "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": [], + "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": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/python-poetry/poetry.git", "long_checkout": false, "py_versions": ["all"] }, "pyanalyze": { - "cli_arguments": [], + "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": [], + "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": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/facebookincubator/ptr.git", "long_checkout": false, "py_versions": ["all"] }, "pytest": { - "cli_arguments": [], - "expect_formatting_changes": false, + "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"] }, "sqlalchemy": { + "no_cli_args_reason": "breaks black with new string parsing - #2188", "cli_arguments": [], "expect_formatting_changes": true, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", @@ -111,28 +112,28 @@ "py_versions": ["all"] }, "tox": { - "cli_arguments": [], - "expect_formatting_changes": false, + "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": [], + "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": [], - "expect_formatting_changes": false, + "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": [], + "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/pypa/warehouse.git", "long_checkout": false, From 5316836393682c6ec6a05d69c549d8167f46d8f6 Mon Sep 17 00:00:00 2001 From: Shota Ray Imaki Date: Thu, 6 May 2021 11:25:43 +0900 Subject: [PATCH 224/680] Simplify GitHub Action entrypoint (#2119) This commit simplifies entrypoint.sh for GitHub Actions by removing duplication of args and black_args (cf. #1909). The reason why #1909 uses the input id black_args is to avoid an overlap with args, but this naming seems redundant. So let me suggest option and src, which are consistent with CLI. Backward compatibility is guaranteed; Users can still use black_args as well. Commit history pre-merge: * Simplify GitHub Action entrypoint (#1909) * Fix prettier * Emit a warning message when `black_args` is used This deprecation should be visible in GitHub Action's UI now. Co-authored-by: Shota Ray Imaki Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- README.md | 14 ++++++++------ action.yml | 12 +++++++++++- action/entrypoint.sh | 21 ++++++--------------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 696bfa7e064..6443569d0d0 100644 --- a/README.md +++ b/README.md @@ -425,15 +425,17 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@stable - with: - args: ". --check" ``` -### Inputs +You may use `options` (Default is `'--check --diff'`) and `src` (Default is `'.'`) as +follows: -#### `black_args` - -**optional**: Black input arguments. Defaults to `. --check --diff`. +```yaml +- uses: psf/black@stable + with: + options: "--check --verbose" + src: "./src" +``` ## Ignoring unmodified files diff --git a/action.yml b/action.yml index 59b16a9fb6c..827e971801b 100644 --- a/action.yml +++ b/action.yml @@ -2,8 +2,18 @@ name: "Black" description: "The uncompromising Python code formatter." author: "Łukasz Langa and contributors to Black" inputs: + options: + description: + "Options passed to black. Use `black --help` to see available options. Default: + '--check'" + required: false + default: "--check --diff" + src: + description: "Source to run black. Default: '.'" + required: false + default: "." black_args: - description: "Black input arguments." + description: "[DEPRECATED] Black input arguments." required: false default: "" branding: diff --git a/action/entrypoint.sh b/action/entrypoint.sh index 50f9472cc5f..fc66e24f53a 100755 --- a/action/entrypoint.sh +++ b/action/entrypoint.sh @@ -1,17 +1,8 @@ -#!/bin/bash -set -e +#!/bin/bash -e -# If no arguments are given use current working directory -black_args=(".") -if [ "$#" -eq 0 ] && [ "${INPUT_BLACK_ARGS}" != "" ]; then - black_args+=(${INPUT_BLACK_ARGS}) -elif [ "$#" -ne 0 ] && [ "${INPUT_BLACK_ARGS}" != "" ]; then - black_args+=($* ${INPUT_BLACK_ARGS}) -elif [ "$#" -ne 0 ] && [ "${INPUT_BLACK_ARGS}" == "" ]; then - black_args+=($*) -else - # Default (if no args provided). - black_args+=("--check" "--diff") -fi +if [ -n $INPUT_BLACK_ARGS ]; then + echo '::warning::Input `with.black_args` is deprecated. Use `with.options` and `with.src` instead.' + black $INPUT_BLACK_ARGS + exit $? -sh -c "black . ${black_args[*]}" +black $INPUT_OPTIONS $INPUT_SRC From 4b7b5ed5b8630a906e2ef3a405134131651a251e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 6 May 2021 15:21:13 -0400 Subject: [PATCH 225/680] Fix broken Action entrypoint (#2202) --- action/entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/action/entrypoint.sh b/action/entrypoint.sh index fc66e24f53a..30bf4eb688f 100755 --- a/action/entrypoint.sh +++ b/action/entrypoint.sh @@ -1,8 +1,9 @@ #!/bin/bash -e -if [ -n $INPUT_BLACK_ARGS ]; then +if [ -n "$INPUT_BLACK_ARGS" ]; then echo '::warning::Input `with.black_args` is deprecated. Use `with.options` and `with.src` instead.' black $INPUT_BLACK_ARGS exit $? +fi black $INPUT_OPTIONS $INPUT_SRC From 1fe2efd8573a63ffc76c69320720d349b21897da Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Fri, 7 May 2021 07:54:21 -0500 Subject: [PATCH 226/680] Do not use gitignore if explicitly passing excludes (#2170) Closes #2164. Changes behavior of how .gitignore is handled. With this change, the rules in .gitignore are only used as a fallback if no exclusion rule is explicitly passed on the command line or in pyproject.toml. Previously they were used regardless if explicit exclusion rules were specified, preventing any overriding of .gitignore rules. Those that depend only on .gitignore for their exclusion rules will not be affected. Those that use both .gitignore and exclude will find that exclude will act more like actually specifying exclude and not just another extra-excludes. If the previous behavior was desired, they should move their rules from exclude to extra-excludes. --- CHANGES.md | 3 ++ src/black/__init__.py | 25 ++++++++++------- tests/data/include_exclude_tests/.gitignore | 1 + .../data/include_exclude_tests/pyproject.toml | 3 ++ tests/test_black.py | 28 +++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 tests/data/include_exclude_tests/.gitignore create mode 100644 tests/data/include_exclude_tests/pyproject.toml diff --git a/CHANGES.md b/CHANGES.md index becf69904fb..b81e6eb4411 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,9 @@ [circumstances](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#pragmatism) in which _Black_ may change the AST (#2159) +- Allow `.gitignore` rules to be overridden by specifying `exclude` in `pyproject.toml` + or on the command line. (#2170) + #### _Packaging_ - Install `primer.json` (used by `black-primer` by default) with black. (#2154) diff --git a/src/black/__init__.py b/src/black/__init__.py index cf257876c03..e47aa21d435 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -492,15 +492,15 @@ def validate_regex( @click.option( "--exclude", type=str, - default=DEFAULT_EXCLUDES, callback=validate_regex, help=( "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." + " Exclusions are calculated first, inclusions later. [default:" + f" {DEFAULT_EXCLUDES}]" ), - show_default=True, + show_default=False, ) @click.option( "--extend-exclude", @@ -587,7 +587,7 @@ def main( quiet: bool, verbose: bool, include: Pattern, - exclude: Pattern, + exclude: Optional[Pattern], extend_exclude: Optional[Pattern], force_exclude: Optional[Pattern], stdin_filename: Optional[str], @@ -662,7 +662,7 @@ def get_sources( quiet: bool, verbose: bool, include: Pattern[str], - exclude: Pattern[str], + exclude: Optional[Pattern[str]], extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: "Report", @@ -673,7 +673,12 @@ def get_sources( root = find_project_root(src) sources: Set[Path] = set() path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) - gitignore = get_gitignore(root) + + if exclude is None: + exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) + gitignore = get_gitignore(root) + else: + gitignore = None for s in src: if s == "-" and stdin_filename: @@ -6215,12 +6220,12 @@ def path_is_excluded( def gen_python_files( paths: Iterable[Path], root: Path, - include: Optional[Pattern[str]], + include: Pattern[str], exclude: Pattern[str], extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: "Report", - gitignore: PathSpec, + gitignore: Optional[PathSpec], ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, @@ -6236,8 +6241,8 @@ def gen_python_files( if normalized_path is None: continue - # First ignore files matching .gitignore - if gitignore.match_file(normalized_path): + # 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") continue diff --git a/tests/data/include_exclude_tests/.gitignore b/tests/data/include_exclude_tests/.gitignore new file mode 100644 index 00000000000..91f34560522 --- /dev/null +++ b/tests/data/include_exclude_tests/.gitignore @@ -0,0 +1 @@ +dont_exclude/ diff --git a/tests/data/include_exclude_tests/pyproject.toml b/tests/data/include_exclude_tests/pyproject.toml new file mode 100644 index 00000000000..9ba7ec26980 --- /dev/null +++ b/tests/data/include_exclude_tests/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/test_black.py b/tests/test_black.py index 7d855cab3d3..9b2bfcd740e 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1418,6 +1418,32 @@ def test_include_exclude(self) -> None: ) self.assertEqual(sorted(expected), sorted(sources)) + def test_gitingore_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 @@ -1705,6 +1731,8 @@ def test_empty_include(self) -> None: 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( From e4b4fb02b91e0f5a60a9678604653aecedff513b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 7 May 2021 15:03:13 +0200 Subject: [PATCH 227/680] Use optional tests for "no_python2" to simplify local testing (#2203) --- pyproject.toml | 5 +- tests/conftest.py | 1 + tests/optional.py | 119 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_black.py | 10 ++-- tox.ini | 4 +- 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/optional.py diff --git a/pyproject.toml b/pyproject.toml index ca75f8f92ef..e89cc7a6c9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,4 +27,7 @@ requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] -markers = ['python2', "without_python2"] \ No newline at end of file +# Option below requires `tests/optional.py` +optional-tests = [ + "no_python2: run when `python2` extra NOT installed", +] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000000..67517268d1b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["tests.optional"] diff --git a/tests/optional.py b/tests/optional.py new file mode 100644 index 00000000000..e12b94cd29e --- /dev/null +++ b/tests/optional.py @@ -0,0 +1,119 @@ +""" +Allows configuring optional test markers in config, see pyproject.toml. + +Run optional tests with `pytest --run-optional=...`. + +Mark tests to run only if an optional test ISN'T selected by prepending the mark with +"no_". + +You can specify a "no_" prefix straight in config, in which case you can mark tests +to run when this tests ISN'T selected by omitting the "no_" prefix. + +Specifying the name of the default behavior in `--run-optional=` is harmless. + +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 + +import pytest +from _pytest.store import StoreKey + +log = logging.getLogger(__name__) + + +if TYPE_CHECKING: + from _pytest.config.argparsing import Parser + from _pytest.config import Config + from _pytest.mark.structures import MarkDecorator + from _pytest.nodes import Node + + +ALL_POSSIBLE_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]() +ENABLED_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]() + + +def pytest_addoption(parser: "Parser") -> None: + group = parser.getgroup("collect") + group.addoption( + "--run-optional", + action="append", + dest="run_optional", + default=None, + help="Optional test markers to run; comma-separated", + ) + parser.addini("optional-tests", "List of optional tests markers", "linelist") + + +def pytest_configure(config: "Config") -> None: + """Optional tests are markers. + + Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks. + """ + ot_ini = config.inicfg.get("optional-tests") or [] + ot_markers = set() + ot_run: Set[str] = set() + if isinstance(ot_ini, str): + ot_ini = ot_ini.strip().split("\n") + marker_re = re.compile(r"^\s*(?Pno_)?(?P\w+)(:\s*(?P.*))?") + for ot in ot_ini: + m = marker_re.match(ot) + if not m: + raise ValueError(f"{ot!r} doesn't match pytest marker syntax") + + marker = (m.group("no") or "") + m.group("marker") + description = m.group("description") + config.addinivalue_line("markers", f"{marker}: {description}") + config.addinivalue_line( + "markers", f"{no(marker)}: run when `{marker}` not passed" + ) + ot_markers.add(marker) + + # collect requested optional tests + passed_args = config.getoption("run_optional") + if passed_args: + ot_run.update(itertools.chain.from_iterable(a.split(",") for a in passed_args)) + ot_run |= {no(excluded) for excluded in ot_markers - ot_run} + ot_markers |= {no(m) for m in ot_markers} + + log.info("optional tests to run:", ot_run) + unknown_tests = ot_run - ot_markers + if unknown_tests: + raise ValueError(f"Unknown optional tests wanted: {unknown_tests!r}") + + store = config._store + store[ALL_POSSIBLE_OPTIONAL_MARKERS] = frozenset(ot_markers) + store[ENABLED_OPTIONAL_MARKERS] = frozenset(ot_run) + + +def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None: + store = config._store + all_possible_optional_markers = store[ALL_POSSIBLE_OPTIONAL_MARKERS] + enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS] + + for item in items: + all_markers_on_test = set(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 + ): + continue + log.info("skipping non-requested optional", item) + item.add_marker(skip_mark(frozenset(optional_markers_on_test))) + + +@lru_cache() +def skip_mark(tests: FrozenSet[str]) -> "MarkDecorator": + names = ", ".join(sorted(tests)) + return pytest.mark.skip(reason=f"Marked with disabled optional tests ({names})") + + +@lru_cache() +def no(name: str) -> str: + if name.startswith("no_"): + return name[len("no_") :] + return "no_" + name diff --git a/tests/test_black.py b/tests/test_black.py index 9b2bfcd740e..b8e526a953e 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -460,11 +460,15 @@ def test_skip_magic_trailing_comma(self) -> None: ) self.assertEqual(expected, actual, msg) - @pytest.mark.without_python2 + @pytest.mark.no_python2 def test_python2_should_fail_without_optional_install(self) -> None: - # python 3.7 and below will install typed-ast and will be able to parse Python 2 if sys.version_info < (3, 8): - return + self.skipTest( + "Python 3.6 and 3.7 will install typed-ast to work and as such will be" + " able to parse Python 2 syntax without explicitly specifying the" + " python2 extra" + ) + source = "x = 1234l" tmp_file = Path(black.dump_to_file(source)) try: diff --git a/tox.ini b/tox.ini index cbb0f75d145..317bf485375 100644 --- a/tox.ini +++ b/tox.ini @@ -9,9 +9,9 @@ deps = commands = pip install -e .[d] coverage erase - coverage run -m pytest tests -m "not python2" {posargs} + coverage run -m pytest tests --run-optional=no_python2 {posargs} pip install -e .[d,python2] - coverage run -m pytest tests -m "not without_python2" {posargs} + coverage run -m pytest tests --run-optional=python2 {posargs} coverage report [testenv:fuzz] From d0e06b53b09248be34c1d5c0fa8f050bff1d201c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 7 May 2021 16:33:36 +0200 Subject: [PATCH 228/680] Mark blackd tests with the `blackd` optional marker (#2204) This is a follow-up of #2203 that uses a pytest marker instead of a bunch of `skipUnless`. Similarly to the Python 2 tests, they are running by default and will crash on an unsuspecting contributor with missing dependencies. This is by design, we WANT contributors to test everything. Unless we actually don't and then we can run: pytest --run-optional=no_blackd Relatedly, bump required aiohttp to 3.6.0 at least to get rid of expected failures on Python 3.8 (see 6b5eb7d4651c7333cc3f5df4bf7aa7a1f1ffb45b). --- Pipfile | 2 +- pyproject.toml | 1 + setup.py | 2 +- tests/test_blackd.py | 32 +++----------------------------- tests/util.py | 14 +------------- 5 files changed, 7 insertions(+), 44 deletions(-) diff --git a/Pipfile b/Pipfile index 40f3d607653..68562f5e53f 100644 --- a/Pipfile +++ b/Pipfile @@ -20,7 +20,7 @@ wheel = ">=0.31.1" black = {editable = true, extras = ["d"], path = "."} [packages] -aiohttp = ">=3.3.2" +aiohttp = ">=3.6.0" aiohttp-cors = "*" appdirs = "*" click = ">=7.1.2" diff --git a/pyproject.toml b/pyproject.toml index e89cc7a6c9b..4d6777ef6ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,4 +30,5 @@ build-backend = "setuptools.build_meta" # 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 newline at end of file diff --git a/setup.py b/setup.py index f1792a46fe8..af93d0f453a 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ def get_long_description() -> str: "mypy_extensions>=0.4.3", ], extras_require={ - "d": ["aiohttp>=3.3.2", "aiohttp-cors"], + "d": ["aiohttp>=3.6.0", "aiohttp-cors"], "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], }, diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 9127297c54f..9ca19d49dc6 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,10 +1,10 @@ import re -import unittest from unittest.mock import patch from click.testing import CliRunner +import pytest -from tests.util import read_data, DETERMINISTIC_HEADER, skip_if_exception +from tests.util import read_data, DETERMINISTIC_HEADER try: import blackd @@ -16,8 +16,8 @@ has_blackd_deps = True +@pytest.mark.blackd class BlackDTestCase(AioHTTPTestCase): - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") def test_blackd_main(self) -> None: with patch("blackd.web.run_app"): result = CliRunner().invoke(blackd.main, []) @@ -28,10 +28,6 @@ def test_blackd_main(self) -> None: async def get_application(self) -> web.Application: return blackd.make_app() - # TODO: remove these decorators once the below is released - # https://github.com/aio-libs/aiohttp/pull/3727 - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_request_needs_formatting(self) -> None: response = await self.client.post("/", data=b"print('hello world')") @@ -39,16 +35,12 @@ async def test_blackd_request_needs_formatting(self) -> None: self.assertEqual(response.charset, "utf8") self.assertEqual(await response.read(), b'print("hello world")\n') - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @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"") - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_request_syntax_error(self) -> None: response = await self.client.post("/", data=b"what even ( is") @@ -59,8 +51,6 @@ async def test_blackd_request_syntax_error(self) -> None: msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", ) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_unsupported_version(self) -> None: response = await self.client.post( @@ -68,8 +58,6 @@ async def test_blackd_unsupported_version(self) -> None: ) self.assertEqual(response.status, 501) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_supported_version(self) -> None: response = await self.client.post( @@ -77,8 +65,6 @@ async def test_blackd_supported_version(self) -> None: ) self.assertEqual(response.status, 200) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_invalid_python_variant(self) -> None: async def check(header_value: str, expected_status: int = 400) -> None: @@ -97,8 +83,6 @@ async def check(header_value: str, expected_status: int = 400) -> None: await check("pypy3.0") await check("jython3.4") - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_pyi(self) -> None: source, expected = read_data("stub.pyi") @@ -108,8 +92,6 @@ async def test_blackd_pyi(self) -> None: self.assertEqual(response.status, 200) self.assertEqual(await response.text(), expected) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_diff(self) -> None: diff_header = re.compile( @@ -128,8 +110,6 @@ async def test_blackd_diff(self) -> None: actual = diff_header.sub(DETERMINISTIC_HEADER, actual) self.assertEqual(actual, expected) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_python_variant(self) -> None: code = ( @@ -166,8 +146,6 @@ async def check(header_value: str, expected_status: int) -> None: await check("py34,py36", 204) await check("34", 204) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_line_length(self) -> None: response = await self.client.post( @@ -175,8 +153,6 @@ async def test_blackd_line_length(self) -> None: ) self.assertEqual(response.status, 200) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_invalid_line_length(self) -> None: response = await self.client.post( @@ -184,8 +160,6 @@ async def test_blackd_invalid_line_length(self) -> None: ) self.assertEqual(response.status, 400) - @skip_if_exception("ClientOSError") - @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop async def test_blackd_response_black_version_header(self) -> None: response = await self.client.post("/") diff --git a/tests/util.py b/tests/util.py index ad9866975ac..3670952ba8c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,8 +1,7 @@ import os import unittest -from contextlib import contextmanager from pathlib import Path -from typing import List, Tuple, Iterator, Any +from typing import List, Tuple, Any import black from functools import partial @@ -45,17 +44,6 @@ def assertFormatEqual(self, expected: str, actual: str) -> None: self.assertMultiLineEqual(expected, actual) -@contextmanager -def skip_if_exception(e: str) -> Iterator[None]: - try: - yield - except Exception as exc: - if exc.__class__.__name__ == e: - unittest.skip(f"Encountered expected exception {exc}, skipping") - else: - raise - - 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")): From 4fc1354aeb6b217cd18dbdb2a0c41373fa9d8056 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Fri, 7 May 2021 10:41:55 -0400 Subject: [PATCH 229/680] Speed up test suite via distributed testing (#2196) * Speed up test suite via distributed testing Since we now run the test suite twice, one with Python 2 and another without, full test runs are getting pretty slow. Let's try to fix that with parallization. Also use verbose mode on CI since more logs is usually better since getting more is quite literally impossible. The main issue we'll face with this is we'll hit https://github.com/pytest-dev/pytest-xdist/issues/620 sometimes (although pretty rarely). I suppose we can test this and see if how bad this bug is for us, and revert if necessary down the line. Also let's have some colours :tada: --- .coveragerc | 1 + .github/workflows/test.yml | 2 +- test_requirements.txt | 2 ++ tox.ini | 4 ++-- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index f05041ca1dc..5577e496a57 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ 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 03adc7f0bea..2cfbab67ce1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Unit tests run: | - tox -e py + tox -e py -- -v --color=yes - name: Publish coverage to Coveralls # If pushed / is a pull request against main repo AND diff --git a/test_requirements.txt b/test_requirements.txt index a1464e90686..31ab2d05fea 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,5 +3,7 @@ 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/tox.ini b/tox.ini index 317bf485375..2379500f55a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,9 +9,9 @@ deps = commands = pip install -e .[d] coverage erase - coverage run -m pytest tests --run-optional=no_python2 {posargs} + pytest tests --run-optional no_python2 --numprocesses auto --cov {posargs} pip install -e .[d,python2] - coverage run -m pytest tests --run-optional=python2 {posargs} + pytest tests --run-optional python2 --numprocesses auto --cov --cov-append {posargs} coverage report [testenv:fuzz] From f2ea461e9e9fa5c47bb61fd72d512c748928badc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 8 May 2021 11:29:47 +0200 Subject: [PATCH 230/680] Refactor `src/black/__init__.py` into many files (#2206) * Move string-related utility to functions to strings.py, const.py * Move Leaf/Node-related functionality to nodes.py * Move comment-related functions to comments.py * Move caching to cache.py and Mode/TargetVersion/Feature to mode.py * Move some leftover functions to nodes.py, comments.py, strings.py * Add missing files to source list for test runs * Move line-related functionality into lines.py, brackets into brackets.py * Move transformers to trans.py * Move file handling, output, parsing, concurrency, debug, and report * Move two more functions to nodes.py * Add CHANGES * Add numeric.py * Add linegen.py * More docstrings * Include new files in tests Co-authored-by: Jelle Zijlstra --- CHANGES.md | 6 + src/black/__init__.py | 6394 ++------------------------------------ src/black/brackets.py | 334 ++ src/black/cache.py | 83 + src/black/comments.py | 269 ++ src/black/concurrency.py | 39 + src/black/const.py | 4 + src/black/debug.py | 48 + src/black/files.py | 241 ++ src/black/linegen.py | 984 ++++++ src/black/lines.py | 734 +++++ src/black/mode.py | 123 + src/black/nodes.py | 843 +++++ src/black/numerics.py | 65 + src/black/output.py | 83 + src/black/parsing.py | 215 ++ src/black/report.py | 100 + src/black/rusty.py | 28 + src/black/strings.py | 216 ++ src/black/trans.py | 1925 ++++++++++++ tests/test_black.py | 43 +- tests/test_format.py | 30 +- tests/util.py | 19 +- 23 files changed, 6586 insertions(+), 6240 deletions(-) create mode 100644 src/black/brackets.py create mode 100644 src/black/cache.py create mode 100644 src/black/comments.py create mode 100644 src/black/concurrency.py create mode 100644 src/black/const.py create mode 100644 src/black/debug.py create mode 100644 src/black/files.py create mode 100644 src/black/linegen.py create mode 100644 src/black/lines.py create mode 100644 src/black/mode.py create mode 100644 src/black/nodes.py create mode 100644 src/black/numerics.py create mode 100644 src/black/output.py create mode 100644 src/black/parsing.py create mode 100644 src/black/report.py create mode 100644 src/black/rusty.py create mode 100644 src/black/strings.py create mode 100644 src/black/trans.py diff --git a/CHANGES.md b/CHANGES.md index b81e6eb4411..8cbdeca44f7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ ## Change Log +### Unreleased + +#### _Black_ + +- Refactor `src/black/__init__.py` into many files (#2206) + ### 21.5b0 #### _Black_ diff --git a/src/black/__init__.py b/src/black/__init__.py index e47aa21d435..c61bc8c1d60 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,167 +1,70 @@ -import ast import asyncio -from abc import ABC, abstractmethod -from collections import defaultdict from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor from contextlib import contextmanager from datetime import datetime from enum import Enum -from functools import lru_cache, partial, wraps import io -import itertools -import logging from multiprocessing import Manager, freeze_support import os from pathlib import Path -import pickle import regex as re import signal import sys -import tempfile import tokenize import traceback from typing import ( Any, - Callable, - Collection, Dict, Generator, - Generic, - Iterable, Iterator, List, Optional, Pattern, - Sequence, Set, Sized, Tuple, - Type, - TypeVar, Union, - cast, - TYPE_CHECKING, ) -from mypy_extensions import mypyc_attr -from appdirs import user_cache_dir -from dataclasses import dataclass, field, replace +from dataclasses import replace import click -import toml - -try: - from typed_ast import ast3, ast27 -except ImportError: - if sys.version_info < (3, 8): - print( - "The typed_ast package is not installed.\n" - "You can install it with `python3 -m pip install typed-ast`.", - file=sys.stderr, - ) - sys.exit(1) - else: - ast3 = ast27 = ast -from pathspec import PathSpec +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.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 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 +from black.output import dump_to_file, diff, color_diff, out, err +from black.report import Report, Changed +from black.files import find_project_root, find_pyproject_toml, parse_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 +from black.parsing import lib2to3_parse, parse_ast, stringify_ast + # lib2to3 fork -from blib2to3.pytree import Node, Leaf, type_repr -from blib2to3 import pygram, pytree -from blib2to3.pgen2 import driver, token -from blib2to3.pgen2.grammar import Grammar -from blib2to3.pgen2.parse import ParseError +from blib2to3.pytree import Node, Leaf +from blib2to3.pgen2 import token from _black_version import version as __version__ -if sys.version_info < (3, 8): - from typing_extensions import Final -else: - from typing import Final - -if TYPE_CHECKING: - import colorama # noqa: F401 - -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_INCLUDES = r"\.pyi?$" -CACHE_DIR = Path(user_cache_dir("black", version=__version__)) -STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" - -STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters. - # types FileContent = str Encoding = str NewLine = str -Depth = int -NodeType = int -ParserState = int -LeafID = int -StringID = int -Priority = int -Index = int -LN = Union[Leaf, Node] -Transformer = Callable[["Line", Collection["Feature"]], Iterator["Line"]] -Timestamp = float -FileSize = int -CacheInfo = Tuple[Timestamp, FileSize] -Cache = Dict[str, CacheInfo] -out = partial(click.secho, bold=True, err=True) -err = partial(click.secho, fg="red", err=True) - -pygram.initialize(CACHE_DIR) -syms = pygram.python_symbols class NothingChanged(UserWarning): """Raised when reformatted code is the same as source.""" -class CannotTransform(Exception): - """Base class for errors raised by Transformers.""" - - -class CannotSplit(CannotTransform): - """A readable split that fits the allotted line length is impossible.""" - - -class InvalidInput(ValueError): - """Raised when input source code fails all parse attempts.""" - - -class BracketMatchError(KeyError): - """Raised when an opening bracket is unable to be matched to a closing bracket.""" - - -T = TypeVar("T") -E = TypeVar("E", bound=Exception) - - -class Ok(Generic[T]): - def __init__(self, value: T) -> None: - self._value = value - - def ok(self) -> T: - return self._value - - -class Err(Generic[E]): - def __init__(self, e: E) -> None: - self._e = e - - def err(self) -> E: - return self._e - - -# The 'Result' return type is used to implement an error-handling model heavily -# influenced by that used by the Rust programming language -# (see https://doc.rust-lang.org/book/ch09-00-error-handling.html). -Result = Union[Ok[T], Err[E]] -TResult = Result[T, CannotTransform] # (T)ransform Result -TMatchResult = TResult[Index] - - class WriteBack(Enum): NO = 0 YES = 1 @@ -182,158 +85,10 @@ def from_configuration( return cls.DIFF if diff else cls.YES -class Changed(Enum): - NO = 0 - CACHED = 1 - YES = 2 - - -class TargetVersion(Enum): - PY27 = 2 - PY33 = 3 - PY34 = 4 - PY35 = 5 - PY36 = 6 - PY37 = 7 - PY38 = 8 - PY39 = 9 - - 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 - TRAILING_COMMA_IN_DEF = 5 - # The following two feature-flags are mutually exclusive, and exactly one should be - # set for every version of python. - ASYNC_IDENTIFIERS = 6 - ASYNC_KEYWORDS = 7 - ASSIGNMENT_EXPRESSIONS = 8 - POS_ONLY_ARGUMENTS = 9 - RELAXED_DECORATORS = 10 - FORCE_OPTIONAL_PARENTHESES = 50 - - -VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { - TargetVersion.PY27: {Feature.ASYNC_IDENTIFIERS}, - 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.PY36: { - Feature.UNICODE_LITERALS, - Feature.F_STRINGS, - Feature.NUMERIC_UNDERSCORES, - Feature.TRAILING_COMMA_IN_CALL, - Feature.TRAILING_COMMA_IN_DEF, - Feature.ASYNC_IDENTIFIERS, - }, - TargetVersion.PY37: { - Feature.UNICODE_LITERALS, - Feature.F_STRINGS, - Feature.NUMERIC_UNDERSCORES, - Feature.TRAILING_COMMA_IN_CALL, - Feature.TRAILING_COMMA_IN_DEF, - Feature.ASYNC_KEYWORDS, - }, - TargetVersion.PY38: { - 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.POS_ONLY_ARGUMENTS, - }, - TargetVersion.PY39: { - 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, - }, -} - - -@dataclass -class Mode: - target_versions: Set[TargetVersion] = field(default_factory=set) - line_length: int = DEFAULT_LINE_LENGTH - string_normalization: bool = True - is_pyi: bool = False - magic_trailing_comma: bool = True - experimental_string_processing: bool = False - - 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) - ) - else: - version_str = "-" - parts = [ - version_str, - str(self.line_length), - str(int(self.string_normalization)), - str(int(self.is_pyi)), - str(int(self.magic_trailing_comma)), - str(int(self.experimental_string_processing)), - ] - return ".".join(parts) - - # Legacy name, left for integrations. FileMode = Mode -def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> bool: - return all(feature in VERSION_TO_FEATURES[version] for version in target_versions) - - -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_pyproject_toml = path_project_root / "pyproject.toml" - if path_pyproject_toml.is_file(): - return str(path_pyproject_toml) - - try: - path_user_pyproject_toml = find_user_pyproject_toml() - return ( - str(path_user_pyproject_toml) - if path_user_pyproject_toml.is_file() - else None - ) - except PermissionError 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 - - -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 toml.TomlDecodeError - """ - pyproject_toml = toml.load(path_config) - config = pyproject_toml.get("tool", {}).get("black", {}) - return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} - - def read_pyproject_toml( ctx: click.Context, param: click.Parameter, value: Optional[str] ) -> Optional[str]: @@ -349,7 +104,7 @@ def read_pyproject_toml( try: config = parse_pyproject_toml(value) - except (toml.TomlDecodeError, OSError) as e: + except (OSError, ValueError) as e: raise click.FileError( filename=value, hint=f"Error reading configuration file: {e}" ) @@ -391,6 +146,17 @@ def target_version_option_callback( return [TargetVersion[val.upper()] for val in v] +def re_compile_maybe_verbose(regex: str) -> Pattern[str]: + """Compile a regular expression string in `regex`. + + If it contains newlines, use verbose mode. + """ + if "\n" in regex: + regex = "(?x)" + regex + compiled: Pattern[str] = re.compile(regex) + return compiled + + def validate_regex( ctx: click.Context, param: click.Parameter, @@ -945,42 +711,6 @@ def format_file_in_place( return True -def color_diff(contents: str) -> str: - """Inject the ANSI color codes to the diff.""" - 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 - elif line.startswith("@@"): - line = "\033[36m" + line + "\033[0m" # cyan, reset - elif line.startswith("+"): - line = "\033[32m" + line + "\033[0m" # green, reset - elif line.startswith("-"): - line = "\033[31m" + line + "\033[0m" # red, reset - lines[i] = line - return "\n".join(lines) - - -def wrap_stream_for_windows( - f: io.TextIOWrapper, -) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]: - """ - Wrap stream with colorama's wrap_stream so colors are shown on Windows. - - If `colorama` is unavailable, the original stream is returned unmodified. - Otherwise, the `wrap_stream()` function determines whether the stream needs - to be wrapped for a Windows environment and will accordingly either return - an `AnsiToWin32` wrapper or the original stream. - """ - try: - from colorama.initialise import wrap_stream - except ImportError: - return f - else: - # Set `strip=False` to avoid needing to modify test_express_diff_with_color. - return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True) - - def format_stdin_to_stdout( fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode ) -> bool: @@ -1127,5896 +857,197 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: return tiow.read(), encoding, newline -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, - ] - - 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 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+ - 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) - # 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" - - for grammar in get_grammars(set(target_versions)): - drv = driver.Driver(grammar, pytree.convert) - try: - result = drv.parse_string(src_txt, True) - break - - except ParseError as pe: - lineno, column = pe.context[1] - lines = src_txt.splitlines() - try: - faulty_line = lines[lineno - 1] - except IndexError: - faulty_line = "" - exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}") - else: - raise exc from None - - if isinstance(result, Leaf): - result = Node(syms.file_input, [result]) - return result - - -def lib2to3_unparse(node: Node) -> str: - """Given a lib2to3 node, return its string representation.""" - code = str(node) - return code - - -class Visitor(Generic[T]): - """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" +def get_features_used(node: Node) -> Set[Feature]: + """Return a set of (relatively) new Python features used in this file. - def visit(self, node: LN) -> Iterator[T]: - """Main method to visit `node` and its children. + Currently looking for: + - f-strings; + - underscores in numeric literals; + - trailing commas after * or ** in function signatures and calls; + - positional only arguments in function signatures and lambdas; + - assignment expression; + - relaxed decorator syntax; + """ + features: Set[Feature] = set() + for n in node.pre_order(): + if n.type == token.STRING: + value_head = n.value[:2] # type: ignore + if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}: + features.add(Feature.F_STRINGS) - It tries to find a `visit_*()` method for the given `node.type`, like - `visit_simple_stmt` for Node objects or `visit_INDENT` for Leaf objects. - If no dedicated `visit_*()` method is found, chooses `visit_default()` - instead. + elif n.type == token.NUMBER: + if "_" in n.value: # type: ignore + features.add(Feature.NUMERIC_UNDERSCORES) - Then yields objects of type `T` from the selected visitor. - """ - if node.type < 256: - name = token.tok_name[node.type] - else: - name = str(type_repr(node.type)) - # We explicitly branch on whether a visitor exists (instead of - # using self.visit_default as the default arg to getattr) in order - # to save needing to create a bound method object and so mypyc can - # generate a native call to visit_default. - visitf = getattr(self, f"visit_{name}", None) - if visitf: - yield from visitf(node) - else: - yield from self.visit_default(node) - - def visit_default(self, node: LN) -> Iterator[T]: - """Default `visit_*()` implementation. Recurses to children of `node`.""" - if isinstance(node, Node): - for child in node.children: - yield from self.visit(child) - - -@dataclass -class DebugVisitor(Visitor[T]): - tree_depth: int = 0 - - 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.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) - else: - _type = token.tok_name.get(node.type, str(node.type)) - 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) + elif n.type == token.SLASH: + if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}: + features.add(Feature.POS_ONLY_ARGUMENTS) - @classmethod - def show(cls, code: Union[str, Leaf, Node]) -> None: - """Pretty-print the lib2to3 AST of a given string of `code`. - - Convenience method for debugging. - """ - v: DebugVisitor[None] = DebugVisitor() - if isinstance(code, str): - code = lib2to3_parse(code) - list(v.visit(code)) - - -WHITESPACE: Final = {token.DEDENT, token.INDENT, token.NEWLINE} -STATEMENT: Final = { - syms.if_stmt, - syms.while_stmt, - syms.for_stmt, - syms.try_stmt, - syms.except_clause, - syms.with_stmt, - syms.funcdef, - syms.classdef, -} -STANDALONE_COMMENT: Final = 153 -token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT" -LOGIC_OPERATORS: Final = {"and", "or"} -COMPARATORS: Final = { - token.LESS, - token.GREATER, - token.EQEQUAL, - token.NOTEQUAL, - token.LESSEQUAL, - token.GREATEREQUAL, -} -MATH_OPERATORS: Final = { - token.VBAR, - token.CIRCUMFLEX, - token.AMPER, - token.LEFTSHIFT, - token.RIGHTSHIFT, - token.PLUS, - token.MINUS, - token.STAR, - token.SLASH, - token.DOUBLESLASH, - token.PERCENT, - token.AT, - token.TILDE, - token.DOUBLESTAR, -} -STARS: Final = {token.STAR, token.DOUBLESTAR} -VARARGS_SPECIALS: Final = STARS | {token.SLASH} -VARARGS_PARENTS: Final = { - syms.arglist, - syms.argument, # double star in arglist - syms.trailer, # single argument to call - syms.typedargslist, - syms.varargslist, # lambdas -} -UNPACKING_PARENTS: Final = { - syms.atom, # single element of a list or set literal - syms.dictsetmaker, - syms.listmaker, - syms.testlist_gexp, - syms.testlist_star_expr, -} -TEST_DESCENDANTS: Final = { - syms.test, - syms.lambdef, - syms.or_test, - syms.and_test, - syms.not_test, - syms.comparison, - syms.star_expr, - syms.expr, - syms.xor_expr, - syms.and_expr, - syms.shift_expr, - syms.arith_expr, - syms.trailer, - syms.term, - syms.power, -} -ASSIGNMENTS: Final = { - "=", - "+=", - "-=", - "*=", - "@=", - "/=", - "%=", - "&=", - "|=", - "^=", - "<<=", - ">>=", - "**=", - "//=", -} -COMPREHENSION_PRIORITY: Final = 20 -COMMA_PRIORITY: Final = 18 -TERNARY_PRIORITY: Final = 16 -LOGIC_PRIORITY: Final = 14 -STRING_PRIORITY: Final = 12 -COMPARATOR_PRIORITY: Final = 10 -MATH_PRIORITIES: Final = { - token.VBAR: 9, - token.CIRCUMFLEX: 8, - token.AMPER: 7, - token.LEFTSHIFT: 6, - token.RIGHTSHIFT: 6, - token.PLUS: 5, - token.MINUS: 5, - token.STAR: 4, - token.SLASH: 4, - token.DOUBLESLASH: 4, - token.PERCENT: 4, - token.AT: 4, - token.TILDE: 3, - token.DOUBLESTAR: 2, -} -DOT_PRIORITY: Final = 1 - - -@dataclass -class BracketTracker: - """Keeps track of brackets on a line.""" - - depth: int = 0 - bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict) - delimiters: Dict[LeafID, Priority] = field(default_factory=dict) - previous: Optional[Leaf] = None - _for_loop_depths: List[int] = field(default_factory=list) - _lambda_argument_depths: List[int] = field(default_factory=list) - invisible: List[Leaf] = field(default_factory=list) - - def mark(self, leaf: Leaf) -> None: - """Mark `leaf` with bracket-related metadata. Keep track of delimiters. - - All leaves receive an int `bracket_depth` field that stores how deep - within brackets a given leaf is. 0 means there are no enclosing brackets - 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 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 - `delimiters` field. - """ - if leaf.type == token.COMMENT: - return - - self.maybe_decrement_after_for_loop_variable(leaf) - self.maybe_decrement_after_lambda_arguments(leaf) - if leaf.type in CLOSING_BRACKETS: - self.depth -= 1 - try: - opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) - except KeyError as e: - raise BracketMatchError( - "Unable to match a closing bracket to the following opening" - f" bracket: {leaf}" - ) from e - leaf.opening_bracket = opening_bracket - if not leaf.value: - self.invisible.append(leaf) - leaf.bracket_depth = self.depth - if self.depth == 0: - delim = is_split_before_delimiter(leaf, self.previous) - if delim and self.previous is not None: - self.delimiters[id(self.previous)] = delim - else: - delim = is_split_after_delimiter(leaf, self.previous) - if delim: - self.delimiters[id(leaf)] = delim - if leaf.type in OPENING_BRACKETS: - self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf - self.depth += 1 - if not leaf.value: - self.invisible.append(leaf) - self.previous = leaf - self.maybe_increment_lambda_arguments(leaf) - self.maybe_increment_for_loop_variable(leaf) - - def any_open_brackets(self) -> bool: - """Return True if there is an yet unmatched open bracket on the line.""" - return bool(self.bracket_match) - - def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: - """Return the highest priority of a delimiter found on the line. - - Values are consistent with what `is_split_*_delimiter()` return. - Raises ValueError on no delimiters. - """ - return max(v for k, v in self.delimiters.items() if k not in exclude) - - def delimiter_count_with_priority(self, priority: Priority = 0) -> int: - """Return the number of delimiters with the given `priority`. - - If no `priority` is passed, defaults to max priority on the line. - """ - if not self.delimiters: - return 0 - - priority = priority or self.max_delimiter_priority() - return sum(1 for p in self.delimiters.values() if p == priority) - - def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool: - """In a for loop, or comprehension, the variables are often unpacks. - - To avoid splitting on the comma in this situation, increase the depth of - tokens between `for` and `in`. - """ - if leaf.type == token.NAME and leaf.value == "for": - self.depth += 1 - self._for_loop_depths.append(self.depth) - return True + elif n.type == token.COLONEQUAL: + features.add(Feature.ASSIGNMENT_EXPRESSIONS) - return False + elif n.type == syms.decorator: + if len(n.children) > 1 and not is_simple_decorator_expression( + n.children[1] + ): + features.add(Feature.RELAXED_DECORATORS) - def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool: - """See `maybe_increment_for_loop_variable` above for explanation.""" - if ( - self._for_loop_depths - and self._for_loop_depths[-1] == self.depth - and leaf.type == token.NAME - and leaf.value == "in" + elif ( + n.type in {syms.typedargslist, syms.arglist} + and n.children + and n.children[-1].type == token.COMMA ): - self.depth -= 1 - self._for_loop_depths.pop() - return True - - return False + if n.type == syms.typedargslist: + feature = Feature.TRAILING_COMMA_IN_DEF + else: + feature = Feature.TRAILING_COMMA_IN_CALL - def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool: - """In a lambda expression, there might be more than one argument. + for ch in n.children: + if ch.type in STARS: + features.add(feature) - To avoid splitting on the comma in this situation, increase the depth of - tokens between `lambda` and `:`. - """ - if leaf.type == token.NAME and leaf.value == "lambda": - self.depth += 1 - self._lambda_argument_depths.append(self.depth) - return True + if ch.type == syms.argument: + for argch in ch.children: + if argch.type in STARS: + features.add(feature) - return False + return features - def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool: - """See `maybe_increment_lambda_arguments` above for explanation.""" - if ( - self._lambda_argument_depths - and self._lambda_argument_depths[-1] == self.depth - and leaf.type == token.COLON - ): - self.depth -= 1 - self._lambda_argument_depths.pop() - return True - return False +def detect_target_versions(node: Node) -> Set[TargetVersion]: + """Detect the version to target based on the nodes used.""" + features = get_features_used(node) + return { + version for version in TargetVersion if features <= VERSION_TO_FEATURES[version] + } - def get_open_lsqb(self) -> Optional[Leaf]: - """Return the most recent opening square bracket (if any).""" - return self.bracket_match.get((self.depth - 1, token.RSQB)) - - -@dataclass -class Line: - """Holds leaves and comments. Can be printed with `str(line)`.""" - - mode: Mode - depth: int = 0 - leaves: List[Leaf] = field(default_factory=list) - # keys ordered like `leaves` - comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict) - bracket_tracker: BracketTracker = field(default_factory=BracketTracker) - inside_brackets: bool = False - should_split_rhs: bool = False - magic_trailing_comma: Optional[Leaf] = None - - def append(self, leaf: Leaf, preformatted: bool = False) -> None: - """Add a new `leaf` to the end of the line. - - Unless `preformatted` is True, the `leaf` will receive a new consistent - whitespace prefix and metadata applied by :class:`BracketTracker`. - Trailing commas are maybe removed, unpacked for loop variables are - demoted from being delimiters. - - Inline comments are put aside. - """ - has_value = leaf.type in BRACKETS or bool(leaf.value.strip()) - if not has_value: - return - - if token.COLON == leaf.type and self.is_class_paren_empty: - del self.leaves[-2:] - if self.leaves and not preformatted: - # 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) - ) - if self.inside_brackets or not preformatted: - self.bracket_tracker.mark(leaf) - if self.mode.magic_trailing_comma: - if self.has_magic_trailing_comma(leaf): - self.magic_trailing_comma = leaf - elif self.has_magic_trailing_comma(leaf, ensure_removable=True): - self.remove_trailing_comma() - if not self.append_comment(leaf): - self.leaves.append(leaf) - - def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None: - """Like :func:`append()` but disallow invalid standalone comment structure. - - Raises ValueError when any `leaf` is appended after a standalone comment - or when a standalone comment is not the first leaf on the line. - """ - if self.bracket_tracker.depth == 0: - if self.is_comment: - raise ValueError("cannot append to standalone comments") - - if self.leaves and leaf.type == STANDALONE_COMMENT: - raise ValueError( - "cannot append standalone comments to a populated line" - ) - self.append(leaf, preformatted=preformatted) - - @property - def is_comment(self) -> bool: - """Is this line a standalone comment?""" - return len(self.leaves) == 1 and self.leaves[0].type == STANDALONE_COMMENT - - @property - def is_decorator(self) -> bool: - """Is this line a decorator?""" - return bool(self) and self.leaves[0].type == token.AT - - @property - def is_import(self) -> bool: - """Is this an import line?""" - return bool(self) and is_import(self.leaves[0]) - - @property - def is_class(self) -> bool: - """Is this line a class definition?""" - return ( - bool(self) - and self.leaves[0].type == token.NAME - and self.leaves[0].value == "class" - ) +def get_future_imports(node: Node) -> Set[str]: + """Return a set of __future__ imports in the file.""" + imports: Set[str] = set() - @property - def is_stub_class(self) -> bool: - """Is this line a class definition with a body consisting only of "..."?""" - return self.is_class and self.leaves[-3:] == [ - Leaf(token.DOT, ".") for _ in range(3) - ] - - @property - def is_def(self) -> bool: - """Is this a function definition? (Also returns True for async defs.)""" - try: - first_leaf = self.leaves[0] - except IndexError: - return False - - try: - second_leaf: Optional[Leaf] = self.leaves[1] - except IndexError: - second_leaf = None - return (first_leaf.type == token.NAME and first_leaf.value == "def") or ( - first_leaf.type == token.ASYNC - and second_leaf is not None - and second_leaf.type == token.NAME - and second_leaf.value == "def" - ) + def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]: + for child in children: + if isinstance(child, Leaf): + if child.type == token.NAME: + yield child.value - @property - def is_class_paren_empty(self) -> bool: - """Is this a class with no base classes but using parentheses? - - Those are unnecessary and should be removed. - """ - return ( - bool(self) - and len(self.leaves) == 4 - and self.is_class - and self.leaves[2].type == token.LPAR - and self.leaves[2].value == "(" - and self.leaves[3].type == token.RPAR - and self.leaves[3].value == ")" - ) + elif child.type == syms.import_as_name: + orig_name = child.children[0] + assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports" + assert orig_name.type == token.NAME, "Invalid syntax parsing imports" + yield orig_name.value - @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(('"""', "'''")) - ) + elif child.type == syms.import_as_names: + yield from get_imports_from_children(child.children) - def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> 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: - return True + else: + raise AssertionError("Invalid syntax parsing imports") - return False + for child in node.children: + if child.type != syms.simple_stmt: + break - def contains_uncollapsable_type_comments(self) -> bool: - ignored_ids = set() - try: - last_leaf = self.leaves[-1] - ignored_ids.add(id(last_leaf)) - if last_leaf.type == token.COMMA or ( - last_leaf.type == token.RPAR and not last_leaf.value + first_child = child.children[0] + if isinstance(first_child, Leaf): + # Continue looking if we see a docstring; otherwise stop. + if ( + len(child.children) == 2 + and first_child.type == token.STRING + and child.children[1].type == token.NEWLINE ): - # When trailing commas or optional parens are inserted by Black for - # consistency, comments after the previous last element are not moved - # (they don't have to, rendering will still be correct). So we ignore - # trailing commas and invisible. - last_leaf = self.leaves[-2] - ignored_ids.add(id(last_leaf)) - except IndexError: - return False - - # A type comment is uncollapsable if it is attached to a leaf - # that isn't at the end of the line (since that could cause it - # to get associated to a different argument) or if there are - # comments before it (since that could cause it to get hidden - # behind a comment. - comment_seen = False - for leaf_id, comments in self.comments.items(): - for comment in comments: - if is_type_comment(comment): - if comment_seen or ( - not is_type_comment(comment, " ignore") - and leaf_id not in ignored_ids - ): - return True - - comment_seen = True - - return False - - def contains_unsplittable_type_ignore(self) -> bool: - if not self.leaves: - return False - - # If a 'type: ignore' is attached to the end of a line, we - # can't split the line, because we can't know which of the - # subexpressions the ignore was meant to apply to. - # - # We only want this to apply to actual physical lines from the - # original source, though: we don't want the presence of a - # 'type: ignore' at the end of a multiline expression to - # justify pushing it all onto one line. Thus we - # (unfortunately) need to check the actual source lines and - # only report an unsplittable 'type: ignore' if this line was - # one line in the original code. - - # Grab the first and last line numbers, skipping generated leaves - first_line = next((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0) - last_line = next( - (leaf.lineno for leaf in reversed(self.leaves) if leaf.lineno != 0), 0 - ) - - if first_line == last_line: - # We look at the last two leaves since a comma or an - # invisible paren could have been added at the end of the - # line. - for node in self.leaves[-2:]: - for comment in self.comments.get(id(node), []): - if is_type_comment(comment, " ignore"): - return True - - return False - - def contains_multiline_strings(self) -> bool: - return any(is_multiline_string(leaf) for leaf in self.leaves) - - def has_magic_trailing_comma( - self, closing: Leaf, ensure_removable: bool = False - ) -> bool: - """Return True if we have a magic trailing comma, that is when: - - there's a trailing comma here - - it's not a one-tuple - Additionally, if ensure_removable: - - it's not from square bracket indexing - """ - if not ( - closing.type in CLOSING_BRACKETS - and self.leaves - and self.leaves[-1].type == token.COMMA - ): - return False - - if closing.type == token.RBRACE: - return True - - if closing.type == token.RSQB: - if not ensure_removable: - return True - comma = self.leaves[-1] - return bool(comma.parent and comma.parent.type == syms.listmaker) + continue - if self.is_import: - return True + break - if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves): - return True + elif first_child.type == syms.import_from: + module_name = first_child.children[1] + if not isinstance(module_name, Leaf) or module_name.value != "__future__": + break - return False + imports |= set(get_imports_from_children(first_child.children[3:])) + else: + break - def append_comment(self, comment: Leaf) -> bool: - """Add an inline or standalone comment to the line.""" - if ( - comment.type == STANDALONE_COMMENT - and self.bracket_tracker.any_open_brackets() - ): - comment.prefix = "" - return False - - if comment.type != token.COMMENT: - return False - - if not self.leaves: - comment.type = STANDALONE_COMMENT - comment.prefix = "" - return False - - last_leaf = self.leaves[-1] - if ( - last_leaf.type == token.RPAR - and not last_leaf.value - and last_leaf.parent - and len(list(last_leaf.parent.leaves())) <= 3 - and not is_type_comment(comment) - ): - # Comments on an optional parens wrapping a single leaf should belong to - # the wrapped node except if it's a type comment. Pinning the comment like - # this avoids unstable formatting caused by comment migration. - if len(self.leaves) < 2: - comment.type = STANDALONE_COMMENT - comment.prefix = "" - return False - - last_leaf = self.leaves[-2] - self.comments.setdefault(id(last_leaf), []).append(comment) - return True + return imports - def comments_after(self, leaf: Leaf) -> List[Leaf]: - """Generate comments that should appear directly after `leaf`.""" - return self.comments.get(id(leaf), []) - def remove_trailing_comma(self) -> None: - """Remove the trailing comma and moves the comments attached to it.""" - trailing_comma = self.leaves.pop() - trailing_comma_comments = self.comments.pop(id(trailing_comma), []) - self.comments.setdefault(id(self.leaves[-1]), []).extend( - trailing_comma_comments +def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: + """Raise AssertionError if `src` and `dst` aren't equivalent.""" + try: + 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}" ) - def is_complex_subscript(self, leaf: Leaf) -> bool: - """Return True iff `leaf` is part of a slice with non-trivial exprs.""" - open_lsqb = self.bracket_tracker.get_open_lsqb() - if open_lsqb is None: - return False - - subscript_start = open_lsqb.next_sibling + try: + dst_ast = parse_ast(dst) + 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}. " + "Please report a bug on https://github.com/psf/black/issues. " + f"This invalid output might be helpful: {log}" + ) from None - if isinstance(subscript_start, Node): - if subscript_start.type == syms.listmaker: - return False + src_ast_str = "\n".join(stringify_ast(src_ast)) + dst_ast_str = "\n".join(stringify_ast(dst_ast)) + if src_ast_str != dst_ast_str: + 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"https://github.com/psf/black/issues. This diff might be helpful: {log}" + ) from None - if subscript_start.type == syms.subscriptlist: - subscript_start = child_towards(subscript_start, leaf) - return subscript_start is not None and any( - n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() - ) - def clone(self) -> "Line": - return Line( - mode=self.mode, - depth=self.depth, - inside_brackets=self.inside_brackets, - should_split_rhs=self.should_split_rhs, - magic_trailing_comma=self.magic_trailing_comma, +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) + if dst != newdst: + log = dump_to_file( + str(mode), + diff(src, dst, "source", "first pass"), + diff(dst, newdst, "first pass", "second pass"), ) + raise AssertionError( + "INTERNAL ERROR: Black produced different code on the second pass of the" + " formatter. Please report a bug on https://github.com/psf/black/issues." + f" This diff might be helpful: {log}" + ) from None - def __str__(self) -> str: - """Render the line.""" - if not self: - return "\n" - indent = " " * self.depth - leaves = iter(self.leaves) - first = next(leaves) - res = f"{first.prefix}{indent}{first.value}" - for leaf in leaves: - res += str(leaf) - for comment in itertools.chain.from_iterable(self.comments.values()): - res += str(comment) +@contextmanager +def nullcontext() -> Iterator[None]: + """Return an empty context manager. - return res + "\n" + To be used like `nullcontext` in Python 3.7. + """ + yield - def __bool__(self) -> bool: - """Return True if the line has leaves or comments.""" - return bool(self.leaves or self.comments) +def patch_click() -> None: + """Make Click not crash. -@dataclass -class EmptyLineTracker: - """Provides a stateful method that returns the number of potential extra - empty lines needed before and after the currently processed line. + 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. - Note: this tracker works on lines that haven't been split yet. It assumes - the prefix of the first leaf consists of optional newlines. Those newlines - are consumed by `maybe_empty_lines()` and included in the computation. + 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. """ + try: + from click import core + from click import _unicodefun # type: ignore + except ModuleNotFoundError: + return - is_pyi: bool = False - previous_line: Optional[Line] = None - previous_after: int = 0 - previous_defs: List[int] = field(default_factory=list) - - def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: - """Return the number of extra empty lines before and after the `current_line`. - - 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) - 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 - ) - self.previous_after = after - self.previous_line = current_line - return before, after - - 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 - if current_line.leaves: - # Consume the first leaf's extra newlines. - first_leaf = current_line.leaves[0] - before = first_leaf.prefix.count("\n") - before = min(before, max_allowed) - first_leaf.prefix = "" - else: - 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 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) - - if ( - self.previous_line - and self.previous_line.is_import - and not current_line.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_triple_quoted_string - ): - return before, 1 - - return before, 0 - - 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) - if self.previous_line is None: - # Don't insert empty lines before the first line in the file. - return 0, 0 - - if self.previous_line.is_decorator: - if self.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 ( - self.previous_line.is_class or self.previous_line.is_def - ): - return 0, 0 - - if ( - self.previous_line.is_comment - and self.previous_line.depth == current_line.depth - and before == 0 - ): - return 0, 0 - - if self.is_pyi: - if self.previous_line.depth > current_line.depth: - newlines = 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: - # No blank line between classes with an empty body - newlines = 0 - else: - newlines = 1 - 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 - else: - newlines = 0 - else: - newlines = 2 - if current_line.depth and newlines: - newlines -= 1 - return newlines, 0 - - -@dataclass -class LineGenerator(Visitor[Line]): - """Generates reformatted Line objects. Empty lines are not emitted. - - Note: destroys the tree it's visiting by mutating prefixes of its leaves - in ways that will no longer stringify to valid Python code on the tree. - """ - - mode: Mode - remove_u_prefix: bool = False - current_line: Line = field(init=False) - - def line(self, indent: int = 0) -> Iterator[Line]: - """Generate a line. - - If the line is empty, only emit if it makes sense. - If the line is too long, split it first and then generate. - - If any lines were generated, set up a new current_line. - """ - if not self.current_line: - self.current_line.depth += indent - return # Line is empty, don't emit. Creating a new one unnecessary. - - complete_line = self.current_line - self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) - yield complete_line - - 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): - if any_open_brackets: - # any comment within brackets is subject to splitting - self.current_line.append(comment) - elif comment.type == token.COMMENT: - # regular trailing comment - self.current_line.append(comment) - yield from self.line() - - else: - # regular standalone comment - yield from self.line() - - self.current_line.append(comment) - yield from self.line() - - normalize_prefix(node, inside_brackets=any_open_brackets) - if self.mode.string_normalization and node.type == token.STRING: - normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix) - normalize_string_quotes(node) - if node.type == token.NUMBER: - normalize_numeric_literal(node) - if node.type not in WHITESPACE: - self.current_line.append(node) - yield from super().visit_default(node) - - def visit_INDENT(self, node: Leaf) -> Iterator[Line]: - """Increase indentation level, maybe yield a line.""" - # In blib2to3 INDENT never holds comments. - yield from self.line(+1) - yield from self.visit_default(node) - - def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: - """Decrease indentation level, maybe yield a line.""" - # The current line might still wait for trailing comments. At DEDENT time - # there won't be any (they would be prefixes on the preceding NEWLINE). - # Emit the line then. - yield from self.line() - - # While DEDENT has no value, its prefix may contain standalone comments - # that belong to the current indentation level. Get 'em. - yield from self.visit_default(node) - - # Finally, emit the dedent. - yield from self.line(-1) - - def visit_stmt( - self, node: Node, keywords: Set[str], parens: Set[str] - ) -> Iterator[Line]: - """Visit a statement. - - This implementation is shared for `if`, `while`, `for`, `try`, `except`, - `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. - - `parens` holds a set of string leaf values immediately after which - invisible parens should be put. - """ - normalize_invisible_parens(node, parens_after=parens) - for child in node.children: - if child.type == token.NAME and child.value in keywords: # type: ignore - yield from self.line() - - 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): - yield from self.visit(node.children[2]) - else: - yield from self.visit_default(node) - - 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) - 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): - yield from self.visit_default(node) - else: - yield from self.line(+1) - yield from self.visit_default(node) - yield from self.line(-1) - - else: - if ( - not self.mode.is_pyi - or not node.parent - or not is_stub_suite(node.parent) - ): - yield from self.line() - yield from self.visit_default(node) - - def visit_async_stmt(self, node: Node) -> Iterator[Line]: - """Visit `async def`, `async for`, `async with`.""" - yield from self.line() - - children = iter(node.children) - for child in children: - yield from self.visit(child) - - if child.type == token.ASYNC: - break - - internal_stmt = next(children) - for child in internal_stmt.children: - yield from self.visit(child) - - def visit_decorators(self, node: Node) -> Iterator[Line]: - """Visit decorators.""" - for child in node.children: - yield from self.line() - yield from self.visit(child) - - def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: - """Remove a semicolon and put the other statement on a separate line.""" - yield from self.line() - - def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]: - """End of file. Process outstanding comments and end with a newline.""" - yield from self.visit_default(leaf) - yield from self.line() - - def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: - if not self.current_line.bracket_tracker.any_open_brackets(): - yield from self.line() - yield from self.visit_default(leaf) - - def visit_factor(self, node: Node) -> Iterator[Line]: - """Force parentheses between a unary op and a binary power: - - -2 ** 8 -> -(2 ** 8) - """ - _operator, operand = node.children - if ( - operand.type == syms.power - and len(operand.children) == 3 - and operand.children[1].type == token.DOUBLESTAR - ): - lpar = Leaf(token.LPAR, "(") - rpar = Leaf(token.RPAR, ")") - index = operand.remove() or 0 - node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) - yield from self.visit_default(node) - - 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. - prefix = get_string_prefix(leaf.value) - docstring = leaf.value[len(prefix) :] # Remove the prefix - quote_char = docstring[0] - # A natural way to remove the outer quotes is to do: - # docstring = docstring.strip(quote_char) - # but that breaks on """""x""" (which is '""x'). - # So we actually need to remove the first character and the next two - # characters but only if they are the same as the first. - quote_len = 1 if docstring[1] != quote_char else 3 - docstring = docstring[quote_len:-quote_len] - - if is_multiline_string(leaf): - indent = " " * 4 * self.current_line.depth - docstring = fix_docstring(docstring, indent) - else: - docstring = docstring.strip() - - if docstring: - # Add some padding if the docstring starts / ends with a quote mark. - if docstring[0] == quote_char: - docstring = " " + docstring - if docstring[-1] == quote_char: - docstring += " " - if docstring[-1] == "\\": - backslash_count = len(docstring) - len(docstring.rstrip("\\")) - if backslash_count % 2: - # Odd number of tailing backslashes, add some padding to - # avoid escaping the closing string quote. - docstring += " " - else: - # Add some padding if the docstring is empty. - docstring = " " - - # We could enforce triple quotes at this point. - quote = quote_char * quote_len - leaf.value = prefix + quote + docstring + quote - - yield from self.visit_default(leaf) - - def __post_init__(self) -> None: - """You are in a twisty little maze of passages.""" - self.current_line = Line(mode=self.mode) - - v = self.visit_stmt - Ø: Set[str] = set() - self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","}) - self.visit_if_stmt = partial( - v, keywords={"if", "else", "elif"}, parens={"if", "elif"} - ) - self.visit_while_stmt = partial(v, keywords={"while", "else"}, parens={"while"}) - self.visit_for_stmt = partial(v, keywords={"for", "else"}, parens={"for", "in"}) - self.visit_try_stmt = partial( - v, keywords={"try", "except", "else", "finally"}, parens=Ø - ) - 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"}) - self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) - self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) - self.visit_async_funcdef = self.visit_async_stmt - self.visit_decorated = self.visit_decorators - - -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} - - -def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 - """Return whitespace prefix if needed for the given `leaf`. - - `complex_subscript` signals whether the given leaf is part of a subscription - which has non-trivial arguments, like arithmetic expressions or function calls. - """ - NO = "" - SPACE = " " - DOUBLESPACE = " " - t = leaf.type - p = leaf.parent - v = leaf.value - if t in ALWAYS_NO_SPACE: - return NO - - if t == token.COMMENT: - return DOUBLESPACE - - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" - if t == token.COLON and p.type not in { - syms.subscript, - syms.subscriptlist, - syms.sliceop, - }: - return NO - - prev = leaf.prev_sibling - if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: - return NO - - if t == token.COLON: - if prevp.type == token.COLON: - return NO - - elif prevp.type != token.COMMA and not complex_subscript: - return NO - - return SPACE - - if prevp.type == token.EQUAL: - if prevp.parent: - if prevp.parent.type in { - syms.arglist, - syms.argument, - syms.parameters, - syms.varargslist, - }: - return NO - - elif prevp.parent.type == syms.typedargslist: - # A bit hacky: if the equal sign has whitespace, it means we - # previously found it's a typed argument. So, we're using - # that, too. - return prevp.prefix - - elif prevp.type in VARARGS_SPECIALS: - if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS): - return NO - - elif prevp.type == token.COLON: - if prevp.parent and prevp.parent.type in {syms.subscript, syms.sliceop}: - return SPACE if complex_subscript else NO - - elif ( - prevp.parent - and prevp.parent.type == syms.factor - and prevp.type in MATH_OPERATORS - ): - return NO - - elif ( - prevp.type == token.RIGHTSHIFT - 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 - ): - # 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 - - elif prev.type in OPENING_BRACKETS: - return NO - - if p.type in {syms.parameters, syms.arglist}: - # untyped function signatures or calls - if not prev or prev.type != token.COMMA: - return NO - - elif p.type == syms.varargslist: - # lambdas - if prev and prev.type != token.COMMA: - return NO - - elif p.type == syms.typedargslist: - # typed function signatures - if not prev: - return NO - - if t == token.EQUAL: - if prev.type != syms.tname: - return NO - - elif prev.type == token.EQUAL: - # A bit hacky: if the equal sign has whitespace, it means we - # previously found it's a typed argument. So, we're using that, too. - return prev.prefix - - elif prev.type != token.COMMA: - return NO - - elif p.type == syms.tname: - # type names - if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type != token.COMMA: - return NO - - elif p.type == syms.trailer: - # attributes and calls - if t == token.LPAR or t == token.RPAR: - 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: - return NO - - elif prev.type != token.COMMA: - return NO - - elif p.type == syms.argument: - # single argument - if t == token.EQUAL: - return NO - - if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type == token.LPAR: - return NO - - elif prev.type in {token.EQUAL} | VARARGS_SPECIALS: - return NO - - elif p.type == syms.decorator: - # decorators - return NO - - elif p.type == syms.dotted_name: - if prev: - return NO - - prevp = preceding_leaf(p) - if not prevp or prevp.type == token.AT or prevp.type == token.DOT: - return NO - - elif p.type == syms.classdef: - if t == token.LPAR: - return NO - - if prev and prev.type == token.LPAR: - return NO - - elif p.type in {syms.subscript, syms.sliceop}: - # indexing - if not prev: - assert p.parent is not None, "subscripts are always parented" - if p.parent.type == syms.subscriptlist: - return SPACE - - return NO - - elif not complex_subscript: - return NO - - elif p.type == syms.atom: - if prev and t == token.DOT: - # dots, but not the first one. - return NO - - elif p.type == syms.dictsetmaker: - # dict unpacking - if prev and prev.type == token.DOUBLESTAR: - return NO - - elif p.type in {syms.factor, syms.star_expr}: - # unary ops - if not prev: - prevp = preceding_leaf(p) - if not prevp or prevp.type in OPENING_BRACKETS: - return NO - - prevp_parent = prevp.parent - assert prevp_parent is not None - if prevp.type == token.COLON and prevp_parent.type in { - syms.subscript, - syms.sliceop, - }: - return NO - - elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument: - return NO - - elif t in {token.NAME, token.NUMBER, token.STRING}: - return NO - - elif p.type == syms.import_from: - if t == token.DOT: - if prev and prev.type == token.DOT: - return NO - - elif t == token.NAME: - if v == "import": - return SPACE - - if prev and prev.type == token.DOT: - return NO - - elif p.type == syms.sliceop: - return NO - - return SPACE - - -def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: - """Return the first leaf that precedes `node`, if any.""" - while node: - res = node.prev_sibling - if res: - if isinstance(res, Leaf): - return res - - try: - return list(res.leaves())[-1] - - except IndexError: - return None - - node = node.parent - return None - - -def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool: - """Return if the `node` and its previous siblings match types against the provided - list of tokens; the provided `node`has its type matched against the last element in - the list. `None` can be used as the first element to declare that the start of the - list is anchored at the start of its parent's children.""" - if not tokens: - return True - if tokens[-1] is None: - return node is None - if not node: - return False - if node.type != tokens[-1]: - return False - return prev_siblings_are(node.prev_sibling, tokens[:-1]) - - -def child_towards(ancestor: Node, descendant: LN) -> Optional[LN]: - """Return the child of `ancestor` that contains `descendant`.""" - node: Optional[LN] = descendant - while node and node.parent != ancestor: - node = node.parent - return node - - -def container_of(leaf: Leaf) -> LN: - """Return `leaf` or one of its ancestors that is the topmost container of it. - - By "container" we mean a node where `leaf` is the very first child. - """ - same_prefix = leaf.prefix - container: LN = leaf - while container: - parent = container.parent - if parent is None: - break - - if parent.children[0].prefix != same_prefix: - break - - if parent.type == syms.file_input: - break - - if parent.prev_sibling is not None and parent.prev_sibling.type in BRACKETS: - break - - container = parent - return container - - -def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break after it. - - The delimiter priorities returned here are from those delimiters that would - cause a line break after themselves. - - Higher numbers are higher priority. - """ - if leaf.type == token.COMMA: - return COMMA_PRIORITY - - return 0 - - -def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break before it. - - The delimiter priorities returned here are from those delimiters that would - cause a line break before themselves. - - Higher numbers are higher priority. - """ - if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS): - # * and ** might also be MATH_OPERATORS but in this case they are not. - # Don't treat them as a delimiter. - return 0 - - if ( - leaf.type == token.DOT - and leaf.parent - and leaf.parent.type not in {syms.import_from, syms.dotted_name} - and (previous is None or previous.type in CLOSING_BRACKETS) - ): - return DOT_PRIORITY - - if ( - leaf.type in MATH_OPERATORS - and leaf.parent - and leaf.parent.type not in {syms.factor, syms.star_expr} - ): - return MATH_PRIORITIES[leaf.type] - - if leaf.type in COMPARATORS: - return COMPARATOR_PRIORITY - - if ( - leaf.type == token.STRING - and previous is not None - and previous.type == token.STRING - ): - return STRING_PRIORITY - - if leaf.type not in {token.NAME, token.ASYNC}: - return 0 - - if ( - leaf.value == "for" - and leaf.parent - and leaf.parent.type in {syms.comp_for, syms.old_comp_for} - or leaf.type == token.ASYNC - ): - if ( - not isinstance(leaf.prev_sibling, Leaf) - or leaf.prev_sibling.value != "async" - ): - return COMPREHENSION_PRIORITY - - if ( - leaf.value == "if" - and leaf.parent - and leaf.parent.type in {syms.comp_if, syms.old_comp_if} - ): - return COMPREHENSION_PRIORITY - - if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test: - return TERNARY_PRIORITY - - if leaf.value == "is": - return COMPARATOR_PRIORITY - - if ( - leaf.value == "in" - and leaf.parent - and leaf.parent.type in {syms.comp_op, syms.comparison} - and not ( - previous is not None - and previous.type == token.NAME - and previous.value == "not" - ) - ): - return COMPARATOR_PRIORITY - - if ( - leaf.value == "not" - and leaf.parent - and leaf.parent.type == syms.comp_op - and not ( - previous is not None - and previous.type == token.NAME - and previous.value == "is" - ) - ): - return COMPARATOR_PRIORITY - - if leaf.value in LOGIC_OPERATORS and leaf.parent: - return LOGIC_PRIORITY - - return 0 - - -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"} - - -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 - in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation - move because it does away with modifying the grammar to include all the - possible places in which comments can be placed. - - The sad consequence for us though is that comments don't "belong" anywhere. - This is why this function generates simple parentless Leaf objects for - comments. We simply don't know what the correct parent should be. - - No matter though, we can live without this. We really only need to - differentiate between inline and standalone comments. The latter don't - share the line with any code. - - 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): - yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) - - -@dataclass -class ProtoComment: - """Describes a piece of syntax that is a comment. - - It's not a :class:`blib2to3.pytree.Leaf` so that: - - * it can be cached (`Leaf` objects should not be reused more than once as - they store their lineno, column, prefix, and parent information); - * `newlines` and `consumed` fields are kept separate from the `value`. This - simplifies handling of special marker comments like ``# fmt: off/on``. - """ - - type: int # token.COMMENT or STANDALONE_COMMENT - 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 - - -@lru_cache(maxsize=4096) -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: - return result - - 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() - if not line: - nlines += 1 - 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 - # as a simple trailing comment. - if line.endswith("\\"): - ignored_lines += 1 - continue - - if index == ignored_lines and not is_endmarker: - comment_type = token.COMMENT # simple trailing comment - else: - comment_type = STANDALONE_COMMENT - comment = make_comment(line) - result.append( - ProtoComment( - type=comment_type, value=comment, newlines=nlines, consumed=consumed - ) - ) - nlines = 0 - return result - - -def make_comment(content: str) -> str: - """Return a consistently formatted comment from the given `content` string. - - All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single - space between the hash sign and the content. - - If `content` didn't start with a hash sign, one is provided. - """ - content = content.rstrip() - if not content: - return "#" - - if content[0] == "#": - content = content[1:] - NON_BREAKING_SPACE = " " - if ( - content - and content[0] == NON_BREAKING_SPACE - and not content.lstrip().startswith("type:") - ): - content = " " + content[1:] # Replace NBSP by a simple space - if content and content[0] not in " !:#'%": - content = " " + content - return "#" + content - - -def transform_line( - line: Line, mode: Mode, features: Collection[Feature] = () -) -> Iterator[Line]: - """Transform a `line`, potentially splitting it into many lines. - - They should fit in the allotted `line_length` but might not be able to. - - `features` are syntactical features that may be used in the output. - """ - if line.is_comment: - yield line - return - - line_str = line_to_string(line) - - def init_st(ST: Type[StringTransformer]) -> StringTransformer: - """Initialize StringTransformer""" - return ST(mode.line_length, mode.string_normalization) - - string_merge = init_st(StringMerger) - string_paren_strip = init_st(StringParenStripper) - string_split = init_st(StringSplitter) - string_paren_wrap = init_st(StringParenWrapper) - - transformers: List[Transformer] - if ( - not line.contains_uncollapsable_type_comments() - 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) - or line.contains_unsplittable_type_ignore() - ) - 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: - transformers = [string_merge, string_paren_strip] - else: - transformers = [] - elif line.is_def: - transformers = [left_hand_split] - else: - - def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: - """Wraps calls to `right_hand_split`. - - The calls increasingly `omit` right-hand trailers (bracket pairs with - content), meaning the trailers get glued together to split on another - 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) - ) - # 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): - yield from lines - return - - # All splits failed, best effort split with no omits. - # 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 - ) - - if mode.experimental_string_processing: - if line.inside_brackets: - transformers = [ - string_merge, - string_paren_strip, - string_split, - delimiter_split, - standalone_comment_split, - string_paren_wrap, - rhs, - ] - else: - transformers = [ - string_merge, - string_paren_strip, - string_split, - string_paren_wrap, - rhs, - ] - else: - if line.inside_brackets: - transformers = [delimiter_split, standalone_comment_split, rhs] - else: - transformers = [rhs] - - for transform in transformers: - # We are accumulating lines in `result` because we might want to abort - # mission and return the original line in the end, or attempt a different - # split altogether. - try: - result = run_transformer(line, transform, mode, features, line_str=line_str) - except CannotTransform: - continue - else: - yield from result - break - - else: - yield line - - -@dataclass # type: ignore -class StringTransformer(ABC): - """ - An implementation of the Transformer protocol that relies on its - subclasses overriding the template methods `do_match(...)` and - `do_transform(...)`. - - This Transformer works exclusively on strings (for example, by merging - or splitting them). - - The following sections can be found among the docstrings of each concrete - StringTransformer subclass. - - Requirements: - Which requirements must be met of the given Line for this - StringTransformer to be applied? - - Transformations: - If the given Line meets all of the above requirements, which string - transformations can you expect to be applied to it by this - StringTransformer? - - Collaborations: - What contractual agreements does this StringTransformer have with other - StringTransfomers? Such collaborations should be eliminated/minimized - as much as possible. - """ - - line_length: int - normalize_strings: bool - __name__ = "StringTransformer" - - @abstractmethod - 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. - OR - * Err(CannotTransform), if a match was not able to be made. - """ - - @abstractmethod - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: - """ - Yields: - * Ok(new_line) where new_line is the new transformed line. - 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. - - Side Effects: - This method should NOT mutate @line directly, but it MAY mutate the - Line's underlying Node structure. (WARNING: If the underlying Node - structure IS altered, then this method should NOT be allowed to - yield an CannotTransform after that point.) - """ - - def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]: - """ - StringTransformer instances have a call signature that mirrors that of - the Transformer type. - - Raises: - CannotTransform(...) if the concrete StringTransformer class is unable - to transform @line. - """ - # Optimization to avoid calling `self.do_match(...)` when the line does - # not contain any string. - if not any(leaf.type == token.STRING for leaf in line.leaves): - raise CannotTransform("There are no strings in this line.") - - match_result = self.do_match(line) - - if isinstance(match_result, Err): - cant_transform = match_result.err() - raise CannotTransform( - f"The string transformer {self.__class__.__name__} does not recognize" - " this line as one that it can transform." - ) from cant_transform - - string_idx = match_result.ok() - - for line_result in self.do_transform(line, string_idx): - if isinstance(line_result, Err): - cant_transform = line_result.err() - raise CannotTransform( - "StringTransformer failed while attempting to transform string." - ) from cant_transform - line = line_result.ok() - yield line - - -@dataclass -class CustomSplit: - """A custom (i.e. manual) string split. - - A single CustomSplit instance represents a single substring. - - Examples: - Consider the following string: - ``` - "Hi there friend." - " This is a custom" - f" string {split}." - ``` - - This string will correspond to the following three CustomSplit instances: - ``` - CustomSplit(False, 16) - CustomSplit(False, 17) - CustomSplit(True, 16) - ``` - """ - - has_prefix: bool - break_idx: int - - -class CustomSplitMapMixin: - """ - This mixin class is used to map merged strings to a sequence of - CustomSplits, which will then be used to re-split the strings iff none of - the resultant substrings go over the configured max line length. - """ - - _Key = Tuple[StringID, str] - _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple) - - @staticmethod - def _get_key(string: str) -> "CustomSplitMapMixin._Key": - """ - Returns: - A unique identifier that is used internally to map @string to a - group of custom splits. - """ - return (id(string), string) - - def add_custom_splits( - self, string: str, custom_splits: Iterable[CustomSplit] - ) -> None: - """Custom Split Map Setter Method - - Side Effects: - Adds a mapping from @string to the custom splits @custom_splits. - """ - key = self._get_key(string) - self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits) - - def pop_custom_splits(self, string: str) -> List[CustomSplit]: - """Custom Split Map Getter Method - - Returns: - * A list of the custom splits that are mapped to @string, if any - exist. - OR - * [], otherwise. - - Side Effects: - Deletes the mapping between @string and its associated custom - splits (which are returned to the caller). - """ - key = self._get_key(string) - - custom_splits = self._CUSTOM_SPLIT_MAP[key] - del self._CUSTOM_SPLIT_MAP[key] - - return list(custom_splits) - - def has_custom_splits(self, string: str) -> bool: - """ - Returns: - True iff @string is associated with a set of custom splits. - """ - key = self._get_key(string) - return key in self._CUSTOM_SPLIT_MAP - - -class StringMerger(CustomSplitMapMixin, StringTransformer): - """StringTransformer that merges strings together. - - Requirements: - (A) The line contains adjacent strings such that ALL of the validation checks - listed in StringMerger.__validate_msg(...)'s docstring pass. - 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 - (B) All line-continuation backslashes are removed from the target string. - - Collaborations: - StringMerger provides custom split information to StringSplitter. - """ - - def do_match(self, line: Line) -> TMatchResult: - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - - for (i, leaf) in enumerate(LL): - if ( - leaf.type == token.STRING - and is_valid_index(i + 1) - and LL[i + 1].type == token.STRING - ): - return Ok(i) - - if leaf.type == token.STRING and "\\\n" in leaf.value: - return Ok(i) - - return TErr("This line has no strings that need merging.") - - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: - new_line = line - rblc_result = self.__remove_backslash_line_continuation_chars( - new_line, string_idx - ) - if isinstance(rblc_result, Ok): - new_line = rblc_result.ok() - - msg_result = self.__merge_string_group(new_line, string_idx) - if isinstance(msg_result, Ok): - new_line = msg_result.ok() - - if isinstance(rblc_result, Err) and isinstance(msg_result, Err): - msg_cant_transform = msg_result.err() - rblc_cant_transform = rblc_result.err() - cant_transform = CannotTransform( - "StringMerger failed to merge any strings in this line." - ) - - # Chain the errors together using `__cause__`. - msg_cant_transform.__cause__ = rblc_cant_transform - cant_transform.__cause__ = msg_cant_transform - - yield Err(cant_transform) - else: - yield Ok(new_line) - - @staticmethod - def __remove_backslash_line_continuation_chars( - line: Line, string_idx: int - ) -> TResult[Line]: - """ - Merge strings that were split across multiple lines using - line-continuation backslashes. - - Returns: - Ok(new_line), if @line contains backslash line-continuation - characters. - OR - Err(CannotTransform), otherwise. - """ - 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) - ): - return TErr( - f"String leaf {string_leaf} does not contain any 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", "") - - return Ok(new_line) - - def __merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: - """ - Merges string group (i.e. set of adjacent strings) where the first - string in the group is `line.leaves[string_idx]`. - - Returns: - Ok(new_line), if ALL of the validation checks found in - __validate_msg(...) pass. - OR - Err(CannotTransform), otherwise. - """ - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - - vresult = self.__validate_msg(line, string_idx) - if isinstance(vresult, Err): - return vresult - - # 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 - - # We will place BREAK_MARK in between every two substrings that we - # merge. We will then later go through our final result and use the - # various instances of BREAK_MARK we find to add the right values to - # the custom split map. - BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@" - - QUOTE = LL[string_idx].value[-1] - - def make_naked(string: str, string_prefix: str) -> str: - """Strip @string (i.e. make it a "naked" string) - - Pre-conditions: - * assert_is_leaf_string(@string) - - Returns: - A string that is identical to @string except that - @string_prefix has been stripped, the surrounding QUOTE - characters have been removed, and any remaining QUOTE - characters have been escaped. - """ - assert_is_leaf_string(string) - - RE_EVEN_BACKSLASHES = r"(?:(?= 0 - ), "Logic error while filling the custom string breakpoint cache." - - temp_string = temp_string[mark_idx + len(BREAK_MARK) :] - breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1 - custom_splits.append(CustomSplit(has_prefix, breakpoint_idx)) - - string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, "")) - - if atom_node is not None: - 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) - - @staticmethod - 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(...). - - Returns: - * Ok(None), if ALL validation checks (listed below) pass. - OR - * Err(CannotTransform), if any of the following are true: - - The target string group does not contain ANY stand-alone comments. - - The target string is not in a string group (i.e. it has no - adjacent strings). - - The string group has more than one inline comment. - - The string group has an inline comment that appears to be a pragma. - - 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. - """ - # We first check for "inner" stand-alone comments (i.e. stand-alone - # comments that have a string leaf before them AND after them). - for inc in [1, -1]: - i = string_idx - found_sa_comment = False - is_valid_index = is_valid_index_factory(line.leaves) - while is_valid_index(i) and line.leaves[i].type in [ - token.STRING, - STANDALONE_COMMENT, - ]: - if line.leaves[i].type == STANDALONE_COMMENT: - found_sa_comment = True - elif found_sa_comment: - return TErr( - "StringMerger does NOT merge string groups which contain " - "stand-alone comments." - ) - - i += inc - - num_of_inline_string_comments = 0 - set_of_prefixes = set() - num_of_strings = 0 - for leaf in line.leaves[string_idx:]: - if leaf.type != token.STRING: - # If the string group is trailed by a comma, we count the - # comments trailing the comma to be one of the string group's - # comments. - if leaf.type == token.COMMA and id(leaf) in line.comments: - num_of_inline_string_comments += 1 - break - - if has_triple_quotes(leaf.value): - return TErr("StringMerger does NOT merge multiline strings.") - - num_of_strings += 1 - prefix = get_string_prefix(leaf.value) - if "r" in prefix: - return TErr("StringMerger does NOT merge raw strings.") - - set_of_prefixes.add(prefix) - - if id(leaf) in line.comments: - num_of_inline_string_comments += 1 - if contains_pragma_comment(line.comments[id(leaf)]): - return TErr("Cannot merge strings which have pragma comments.") - - if num_of_strings < 2: - return TErr( - f"Not enough strings to merge (num_of_strings={num_of_strings})." - ) - - if num_of_inline_string_comments > 1: - return TErr( - f"Too many inline string comments ({num_of_inline_string_comments})." - ) - - if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}: - return TErr(f"Too many different prefixes ({set_of_prefixes}).") - - return Ok(None) - - -class StringParenStripper(StringTransformer): - """StringTransformer that strips surrounding parentheses from strings. - - Requirements: - The line contains a string which is surrounded by parentheses and: - - The target string is NOT the only argument to a function call. - - The target string is NOT a "pointless" string. - - If the target string contains a PERCENT, the brackets are not - preceeded or followed by an operator with higher precedence than - PERCENT. - - Transformations: - The parentheses mentioned in the 'Requirements' section are stripped. - - Collaborations: - StringParenStripper has its own inherent usefulness, but it is also - relied on to clean up the parentheses created by StringParenWrapper (in - the event that they are no longer needed). - """ - - def do_match(self, line: Line) -> TMatchResult: - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - - for (idx, leaf) in enumerate(LL): - # Should be a string... - if leaf.type != token.STRING: - continue - - # If this is a "pointless" string... - if ( - leaf.parent - and leaf.parent.parent - and leaf.parent.parent.type == syms.simple_stmt - ): - continue - - # Should be preceded by a non-empty LPAR... - if ( - not is_valid_index(idx - 1) - or LL[idx - 1].type != token.LPAR - or is_empty_lpar(LL[idx - 1]) - ): - continue - - # That LPAR should NOT be preceded by a function name or a closing - # bracket (which could be a function which returns a function or a - # list/dictionary that contains a function)... - if is_valid_index(idx - 2) and ( - LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS - ): - continue - - string_idx = idx - - # Skip the string trailer, if one exists. - string_parser = StringParser() - next_idx = string_parser.parse(LL, string_idx) - - # if the leaves in the parsed string include a PERCENT, we need to - # make sure the initial LPAR is NOT preceded by an operator with - # higher or equal precedence to PERCENT - if is_valid_index(idx - 2): - # mypy can't quite follow unless we name this - before_lpar = LL[idx - 2] - if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and ( - ( - before_lpar.type - in { - token.STAR, - token.AT, - token.SLASH, - token.DOUBLESLASH, - token.PERCENT, - token.TILDE, - token.DOUBLESTAR, - token.AWAIT, - token.LSQB, - token.LPAR, - } - ) - or ( - # only unary PLUS/MINUS - before_lpar.parent - and before_lpar.parent.type == syms.factor - and (before_lpar.type in {token.PLUS, token.MINUS}) - ) - ): - continue - - # Should be followed by a non-empty RPAR... - if ( - is_valid_index(next_idx) - and LL[next_idx].type == token.RPAR - and not is_empty_rpar(LL[next_idx]) - ): - # That RPAR should NOT be followed by anything with higher - # precedence than PERCENT - if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { - token.DOUBLESTAR, - token.LSQB, - token.LPAR, - token.DOT, - }: - continue - - return Ok(string_idx) - - return TErr("This line has no strings wrapped in parens.") - - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: - LL = line.leaves - - string_parser = StringParser() - rpar_idx = string_parser.parse(LL, string_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 - - 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) - - 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) - - append_leaves( - new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] - ) - - LL[rpar_idx].remove() - - yield Ok(new_line) - - -class BaseStringSplitter(StringTransformer): - """ - Abstract class for StringTransformers which transform a Line's strings by splitting - them or placing them on their own lines where necessary to avoid going over - the configured line length. - - 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 - * The target string is NOT a "pointless" string (i.e. a string that has - no parent or siblings). - AND - * The target string is not followed by an inline comment that appears - to be a pragma. - AND - * The target string is not a multiline (i.e. triple-quote) string. - """ - - @abstractmethod - def do_splitter_match(self, line: Line) -> TMatchResult: - """ - BaseStringSplitter asks its clients to override this method instead of - `StringTransformer.do_match(...)`. - - Follows the same protocol as `StringTransformer.do_match(...)`. - - Refer to `help(StringTransformer.do_match)` for more information. - """ - - def do_match(self, line: Line) -> TMatchResult: - match_result = self.do_splitter_match(line) - if isinstance(match_result, Err): - return match_result - - string_idx = match_result.ok() - vresult = self.__validate(line, string_idx) - if isinstance(vresult, Err): - return vresult - - return match_result - - def __validate(self, line: Line, string_idx: int) -> TResult[None]: - """ - Checks that @line meets all of the requirements listed in this classes' - docstring. Refer to `help(BaseStringSplitter)` for a detailed - description of those requirements. - - Returns: - * Ok(None), if ALL of the requirements are met. - OR - * Err(CannotTransform), if ANY of the requirements are NOT met. - """ - LL = line.leaves - - string_leaf = LL[string_idx] - - max_string_length = self.__get_max_string_length(line, string_idx) - if len(string_leaf.value) <= max_string_length: - return TErr( - "The string itself is not what is causing this line to be too long." - ) - - if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [ - token.STRING, - token.NEWLINE, - ]: - return TErr( - f"This string ({string_leaf.value}) appears to be pointless (i.e. has" - " no parent)." - ) - - if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment( - line.comments[id(line.leaves[string_idx])] - ): - return TErr( - "Line appears to end with an inline pragma comment. Splitting the line" - " could modify the pragma's behavior." - ) - - if has_triple_quotes(string_leaf.value): - return TErr("We cannot split multiline strings.") - - return Ok(None) - - def __get_max_string_length(self, line: Line, string_idx: int) -> int: - """ - Calculates the max string length used when attempting to determine - whether or not the target string is responsible for causing the line to - go over the line length limit. - - WARNING: This method is tightly coupled to both StringSplitter and - (especially) StringParenWrapper. There is probably a better way to - accomplish what is being done here. - - Returns: - max_string_length: such that `line.leaves[string_idx].value > - max_string_length` implies that the target string IS responsible - for causing this line to exceed the line length limit. - """ - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - - # We use the shorthand "WMA4" in comments to abbreviate "We must - # account for". When giving examples, we use STRING to mean some/any - # valid string. - # - # Finally, we use the following convenience variables: - # - # P: The leaf that is before the target string leaf. - # N: The leaf that is after the target string leaf. - # NN: The leaf that is after N. - - # WMA4 the whitespace at the beginning of the line. - offset = line.depth * 4 - - if is_valid_index(string_idx - 1): - p_idx = string_idx - 1 - if ( - LL[string_idx - 1].type == token.LPAR - and LL[string_idx - 1].value == "" - and string_idx >= 2 - ): - # If the previous leaf is an empty LPAR placeholder, we should skip it. - p_idx -= 1 - - P = LL[p_idx] - if P.type == token.PLUS: - # WMA4 a space and a '+' character (e.g. `+ STRING`). - offset += 2 - - if P.type == token.COMMA: - # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`]. - offset += 3 - - if P.type in [token.COLON, token.EQUAL, token.NAME]: - # This conditional branch is meant to handle dictionary keys, - # variable assignments, 'return STRING' statement lines, and - # 'else STRING' ternary expression lines. - - # WMA4 a single space. - offset += 1 - - # WMA4 the lengths of any leaves that came before that space, - # but after any closing bracket before that space. - for leaf in reversed(LL[: p_idx + 1]): - offset += len(str(leaf)) - if leaf.type in CLOSING_BRACKETS: - break - - if is_valid_index(string_idx + 1): - N = LL[string_idx + 1] - if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2: - # If the next leaf is an empty RPAR placeholder, we should skip it. - N = LL[string_idx + 2] - - if N.type == token.COMMA: - # WMA4 a single comma at the end of the string (e.g `STRING,`). - offset += 1 - - if is_valid_index(string_idx + 2): - NN = LL[string_idx + 2] - - if N.type == token.DOT and NN.type == token.NAME: - # This conditional branch is meant to handle method calls invoked - # off of a string literal up to and including the LPAR character. - - # WMA4 the '.' character. - offset += 1 - - if ( - is_valid_index(string_idx + 3) - and LL[string_idx + 3].type == token.LPAR - ): - # WMA4 the left parenthesis character. - offset += 1 - - # WMA4 the length of the method's name. - offset += len(NN.value) - - has_comments = False - for comment_leaf in line.comments_after(LL[string_idx]): - if not has_comments: - has_comments = True - # WMA4 two spaces before the '#' character. - offset += 2 - - # WMA4 the length of the inline comment. - offset += len(comment_leaf.value) - - max_string_length = self.line_length - offset - return max_string_length - - -class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): - """ - StringTransformer that splits "atom" strings (i.e. strings which exist on - lines by themselves). - - Requirements: - * The line consists ONLY of a single string (with the exception of a - '+' symbol which MAY exist at the start of the line), MAYBE a string - trailer, and MAYBE a trailing comma. - AND - * All of the requirements listed in BaseStringSplitter's docstring. - - Transformations: - The string mentioned in the 'Requirements' section is split into as - many substrings as necessary to adhere to the configured line length. - - In the final set of substrings, no substring should be smaller than - MIN_SUBSTR_SIZE characters. - - The string will ONLY be split on spaces (i.e. each new substring should - start with a space). Note that the string will NOT be split on a space - which is escaped with a backslash. - - If the string is an f-string, it will NOT be split in the middle of an - f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x - else bar()} is an f-expression). - - If the string that is being split has an associated set of custom split - records and those custom splits will NOT result in any line going over - the configured line length, those custom splits are used. Otherwise the - string is split as late as possible (from left-to-right) while still - adhering to the transformation rules listed above. - - Collaborations: - StringSplitter relies on StringMerger to construct the appropriate - CustomSplit objects and add them to the custom split map. - """ - - MIN_SUBSTR_SIZE = 6 - # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. - RE_FEXPR = r""" - (? TMatchResult: - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - - idx = 0 - - # The first leaf MAY be a '+' symbol... - if is_valid_index(idx) and LL[idx].type == token.PLUS: - idx += 1 - - # The next/first leaf MAY be an empty LPAR... - if is_valid_index(idx) and is_empty_lpar(LL[idx]): - idx += 1 - - # The next/first leaf MUST be a string... - if not is_valid_index(idx) or LL[idx].type != token.STRING: - return TErr("Line does not start with a string.") - - string_idx = idx - - # Skip the string trailer, if one exists. - string_parser = StringParser() - idx = string_parser.parse(LL, string_idx) - - # That string MAY be followed by an empty RPAR... - if is_valid_index(idx) and is_empty_rpar(LL[idx]): - idx += 1 - - # That string / empty RPAR leaf MAY be followed by a comma... - if is_valid_index(idx) and LL[idx].type == token.COMMA: - idx += 1 - - # But no more leaves are allowed... - if is_valid_index(idx): - return TErr("This line does not end with a string.") - - return Ok(string_idx) - - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: - LL = line.leaves - - QUOTE = LL[string_idx].value[-1] - - is_valid_index = is_valid_index_factory(LL) - insert_str_child = insert_str_child_factory(LL[string_idx]) - - prefix = get_string_prefix(LL[string_idx].value) - - # We MAY choose to drop the 'f' prefix from substrings that don't - # 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 - ) - - first_string_line = True - starts_with_plus = LL[0].type == token.PLUS - - def line_needs_plus() -> bool: - return first_string_line and starts_with_plus - - def maybe_append_plus(new_line: Line) -> None: - """ - Side Effects: - If @line starts with a plus and this is the first line we are - constructing, this function appends a PLUS leaf to @new_line - and replaces the old PLUS leaf in the node structure. Otherwise - this function does nothing. - """ - if line_needs_plus(): - plus_leaf = Leaf(token.PLUS, "+") - replace_child(LL[0], plus_leaf) - new_line.append(plus_leaf) - - ends_with_comma = ( - is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA - ) - - def max_last_string() -> int: - """ - Returns: - The max allowed length of the string value used for the last - line we will construct. - """ - result = self.line_length - result -= line.depth * 4 - result -= 1 if ends_with_comma else 0 - result -= 2 if line_needs_plus() else 0 - return result - - # --- Calculate Max Break Index (for string value) - # We start with the line length limit - max_break_idx = self.line_length - # The last index of a string of length N is N-1. - max_break_idx -= 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: - yield TErr( - f"Unable to split {LL[string_idx].value} at such high of a line depth:" - f" {line.depth}" - ) - return - - # Check if StringMerger registered any custom splits. - custom_splits = self.pop_custom_splits(LL[string_idx].value) - # We use them ONLY if none of them would produce lines that exceed the - # line limit. - use_custom_breakpoints = bool( - custom_splits - and all(csplit.break_idx <= max_break_idx for csplit in custom_splits) - ) - - # Temporary storage for the remaining chunk of the string line that - # can't fit onto the line currently being constructed. - rest_value = LL[string_idx].value - - def more_splits_should_be_made() -> bool: - """ - Returns: - True iff `rest_value` (the remaining string value from the last - split), should be split again. - """ - if use_custom_breakpoints: - return len(custom_splits) > 1 - else: - return len(rest_value) > max_last_string() - - string_line_results: List[Ok[Line]] = [] - while more_splits_should_be_made(): - if use_custom_breakpoints: - # Custom User Split (manual) - csplit = custom_splits.pop(0) - break_idx = csplit.break_idx - else: - # Algorithmic Split (automatic) - max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx - 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 - # and this string has custom splits registered to it, we - # fall back to using them--which means we have to start - # over from the beginning. - if custom_splits: - rest_value = LL[string_idx].value - string_line_results = [] - first_string_line = True - use_custom_breakpoints = True - continue - - # Otherwise, we stop splitting here. - break - - break_idx = maybe_break_idx - - # --- Construct `next_value` - next_value = rest_value[:break_idx] + QUOTE - if ( - # Are we allowed to try to drop a pointless 'f' prefix? - drop_pointless_f_prefix - # If we are, will we be successful? - and next_value != self.__normalize_f_string(next_value, prefix) - ): - # If the current custom split did NOT originally use a prefix, - # then `csplit.break_idx` will be off by one after removing - # the 'f' prefix. - break_idx = ( - break_idx + 1 - if use_custom_breakpoints and not csplit.has_prefix - else break_idx - ) - next_value = rest_value[:break_idx] + QUOTE - next_value = self.__normalize_f_string(next_value, prefix) - - # --- Construct `next_leaf` - next_leaf = Leaf(token.STRING, next_value) - insert_str_child(next_leaf) - self.__maybe_normalize_string_quotes(next_leaf) - - # --- Construct `next_line` - next_line = line.clone() - maybe_append_plus(next_line) - next_line.append(next_leaf) - string_line_results.append(Ok(next_line)) - - rest_value = prefix + QUOTE + rest_value[break_idx:] - first_string_line = False - - yield from string_line_results - - if drop_pointless_f_prefix: - rest_value = self.__normalize_f_string(rest_value, prefix) - - rest_leaf = Leaf(token.STRING, rest_value) - insert_str_child(rest_leaf) - - # NOTE: I could not find a test case that verifies that the following - # line is actually necessary, but it seems to be. Otherwise we risk - # not normalizing the last substring, right? - self.__maybe_normalize_string_quotes(rest_leaf) - - last_line = line.clone() - maybe_append_plus(last_line) - - # If there are any leaves to the right of the target string... - if is_valid_index(string_idx + 1): - # We use `temp_value` here to determine how long the last line - # would be if we were to append all the leaves to the right of the - # target string to the last string line. - temp_value = rest_value - for leaf in LL[string_idx + 1 :]: - temp_value += str(leaf) - if leaf.type == token.LPAR: - break - - # Try to fit them all on the same line with the last substring... - if ( - len(temp_value) <= max_last_string() - or LL[string_idx + 1].type == token.COMMA - ): - last_line.append(rest_leaf) - append_leaves(last_line, line, LL[string_idx + 1 :]) - yield Ok(last_line) - # Otherwise, place the last substring on one line and everything - # else on a line below that... - else: - last_line.append(rest_leaf) - yield Ok(last_line) - - non_string_line = line.clone() - append_leaves(non_string_line, line, LL[string_idx + 1 :]) - yield Ok(non_string_line) - # Else the target string was the last leaf... - else: - last_line.append(rest_leaf) - last_line.comments = line.comments.copy() - yield Ok(last_line) - - def __get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]: - """ - This method contains the algorithm that StringSplitter uses to - determine which character to split each string at. - - Args: - @string: The substring that we are attempting to split. - @max_break_idx: The ideal break index. We will return this value if it - meets all the necessary conditions. In the likely event that it - doesn't we will try to find the closest index BELOW @max_break_idx - that does. If that fails, we will expand our search by also - considering all valid indices ABOVE @max_break_idx. - - Pre-Conditions: - * assert_is_leaf_string(@string) - * 0 <= @max_break_idx < len(@string) - - Returns: - break_idx, if an index is able to be found that meets all of the - conditions listed in the 'Transformations' section of this classes' - docstring. - OR - None, otherwise. - """ - is_valid_index = is_valid_index_factory(string) - - assert is_valid_index(max_break_idx) - assert_is_leaf_string(string) - - _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None - - def fexpr_slices() -> Iterator[Tuple[Index, Index]]: - """ - Yields: - All ranges of @string which, if @string were to be split there, - would result in the splitting of an f-expression (which is NOT - allowed). - """ - nonlocal _fexpr_slices - - if _fexpr_slices is None: - _fexpr_slices = [] - for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE): - _fexpr_slices.append(match.span()) - - yield from _fexpr_slices - - is_fstring = "f" in get_string_prefix(string) - - def breaks_fstring_expression(i: Index) -> bool: - """ - Returns: - True iff returning @i would result in the splitting of an - f-expression (which is NOT allowed). - """ - if not is_fstring: - return False - - for (start, end) in fexpr_slices(): - if start <= i < end: - return True - - return False - - 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. - """ - is_space = string[i] == " " - - is_not_escaped = True - j = i - 1 - while is_valid_index(j) and string[j] == "\\": - is_not_escaped = not is_not_escaped - j -= 1 - - is_big_enough = ( - len(string[i:]) >= self.MIN_SUBSTR_SIZE - and len(string[:i]) >= self.MIN_SUBSTR_SIZE - ) - return ( - is_space - and is_not_escaped - and is_big_enough - and not breaks_fstring_expression(i) - ) - - # First, we check all indices BELOW @max_break_idx. - break_idx = max_break_idx - while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx): - break_idx -= 1 - - if not passes_all_checks(break_idx): - # If that fails, we check all indices ABOVE @max_break_idx. - # - # If we are able to find a valid index here, the next line is going - # to be longer than the specified line length, but it's probably - # better than doing nothing at all. - break_idx = max_break_idx + 1 - while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx): - break_idx += 1 - - if not is_valid_index(break_idx) or not passes_all_checks(break_idx): - return None - - return break_idx - - def __maybe_normalize_string_quotes(self, leaf: Leaf) -> None: - if self.normalize_strings: - normalize_string_quotes(leaf) - - def __normalize_f_string(self, string: str, prefix: str) -> str: - """ - Pre-Conditions: - * assert_is_leaf_string(@string) - - Returns: - * If @string is an f-string that contains no f-expressions, we - return a string identical to @string except that the 'f' prefix - has been stripped and all double braces (i.e. '{{' or '}}') have - been normalized (i.e. turned into '{' or '}'). - OR - * Otherwise, we return @string. - """ - assert_is_leaf_string(string) - - if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE): - new_prefix = prefix.replace("f", "") - - temp = string[len(prefix) :] - temp = re.sub(r"\{\{", "{", temp) - temp = re.sub(r"\}\}", "}", temp) - new_string = temp - - return f"{new_prefix}{new_string}" - else: - return string - - -class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): - """ - StringTransformer that splits non-"atom" strings (i.e. strings that do not - exist on lines by themselves). - - Requirements: - All of the requirements listed in BaseStringSplitter's docstring in - addition to the requirements listed below: - - * The line is a return/yield statement, which returns/yields a string. - 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 - * The line is an assert statement, which ends with a string. - 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 - * The line is a dictionary key assignment where some valid key is being - assigned the value of some string. - - Transformations: - The chosen string is wrapped in parentheses and then split at the LPAR. - - We then have one line which ends with an LPAR and another line that - starts with the chosen string. The latter line is then split again at - the RPAR. This results in the RPAR (and possibly a trailing comma) - being placed on its own line. - - NOTE: If any leaves exist to the right of the chosen string (except - for a trailing comma, which would be placed after the RPAR), those - leaves are placed inside the parentheses. In effect, the chosen - string is not necessarily being "wrapped" by parentheses. We can, - however, count on the LPAR being placed directly before the chosen - string. - - In other words, StringParenWrapper creates "atom" strings. These - can then be split again by StringSplitter, if necessary. - - Collaborations: - In the event that a string line split by StringParenWrapper is - changed such that it no longer needs to be given its own line, - StringParenWrapper relies on StringParenStripper to clean up the - parentheses it created. - """ - - def do_splitter_match(self, line: Line) -> TMatchResult: - LL = line.leaves - - string_idx = ( - self._return_match(LL) - or self._else_match(LL) - or self._assert_match(LL) - or self._assign_match(LL) - or self._dict_match(LL) - ) - - if string_idx is not None: - string_value = line.leaves[string_idx].value - # If the string has no spaces... - if " " not 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: - # 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. - return TErr( - "We do not wrap long strings in parentheses when the" - " resultant line would still be over the specified line" - " length and can't be split further by StringSplitter." - ) - return Ok(string_idx) - - return TErr("This line does not contain any non-atomic strings.") - - @staticmethod - def _return_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 return/yield statement - requirements listed in the 'Requirements' section of this classes' - docstring. - OR - None, otherwise. - """ - # If this line is apart 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 - ].value in ["return", "yield"]: - is_valid_index = is_valid_index_factory(LL) - - idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 - # The next visible leaf MUST contain a string... - if is_valid_index(idx) and LL[idx].type == token.STRING: - return idx - - return None - - @staticmethod - def _else_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 ternary expression - requirements listed in the 'Requirements' section of this classes' - docstring. - OR - None, otherwise. - """ - # If this line is apart of a ternary expression and the first leaf - # contains the "else" keyword... - if ( - parent_type(LL[0]) == syms.test - and LL[0].type == token.NAME - and LL[0].value == "else" - ): - is_valid_index = is_valid_index_factory(LL) - - idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 - # The next visible leaf MUST contain a string... - if is_valid_index(idx) and LL[idx].type == token.STRING: - return idx - - return None - - @staticmethod - def _assert_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 assert statement - requirements listed in the 'Requirements' section of this classes' - docstring. - OR - None, otherwise. - """ - # If this line is apart 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) - - 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 - - # That comma MUST be followed by a string... - if is_valid_index(idx) and LL[idx].type == token.STRING: - string_idx = idx - - # Skip the string trailer, if one exists. - string_parser = StringParser() - idx = string_parser.parse(LL, string_idx) - - # But no more leaves are allowed... - if not is_valid_index(idx): - return string_idx - - return None - - @staticmethod - def _assign_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 assignment statement - requirements listed in the 'Requirements' section of this classes' - docstring. - OR - None, otherwise. - """ - # If this line is apart 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] - and LL[0].type == token.NAME - ): - is_valid_index = is_valid_index_factory(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 - - # That symbol MUST be followed by a string... - if is_valid_index(idx) and LL[idx].type == token.STRING: - string_idx = idx - - # Skip the string trailer, if one exists. - string_parser = StringParser() - idx = string_parser.parse(LL, string_idx) - - # The next leaf MAY be a comma iff this line is apart - # of a function argument... - if ( - parent_type(LL[0]) == syms.argument - and is_valid_index(idx) - and LL[idx].type == token.COMMA - ): - idx += 1 - - # But no more leaves are allowed... - if not is_valid_index(idx): - return string_idx - - return None - - @staticmethod - def _dict_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. - 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)]: - is_valid_index = is_valid_index_factory(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 - - # That colon MUST be followed by a string... - if is_valid_index(idx) and LL[idx].type == token.STRING: - string_idx = idx - - # Skip the string trailer, if one exists. - string_parser = StringParser() - idx = string_parser.parse(LL, string_idx) - - # That string MAY be followed by a comma... - if is_valid_index(idx) and LL[idx].type == token.COMMA: - idx += 1 - - # But no more leaves are allowed... - if not is_valid_index(idx): - return string_idx - - return None - - def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: - LL = line.leaves - - is_valid_index = is_valid_index_factory(LL) - insert_str_child = insert_str_child_factory(LL[string_idx]) - - comma_idx = -1 - ends_with_comma = False - if LL[comma_idx].type == token.COMMA: - ends_with_comma = True - - leaves_to_steal_comments_from = [LL[string_idx]] - if ends_with_comma: - leaves_to_steal_comments_from.append(LL[comma_idx]) - - # --- First Line - first_line = line.clone() - left_leaves = LL[:string_idx] - - # We have to remember to account for (possibly invisible) LPAR and RPAR - # leaves that already wrapped the target string. If these leaves do - # exist, we will replace them with our own LPAR and RPAR leaves. - old_parens_exist = False - if left_leaves and left_leaves[-1].type == token.LPAR: - old_parens_exist = True - leaves_to_steal_comments_from.append(left_leaves[-1]) - left_leaves.pop() - - append_leaves(first_line, line, left_leaves) - - lpar_leaf = Leaf(token.LPAR, "(") - if old_parens_exist: - replace_child(LL[string_idx - 1], lpar_leaf) - else: - insert_str_child(lpar_leaf) - first_line.append(lpar_leaf) - - # We throw inline comments that were originally to the right of the - # target string to the top line. They will now be shown to the right of - # the LPAR. - for leaf in leaves_to_steal_comments_from: - for comment_leaf in line.comments_after(leaf): - first_line.append(comment_leaf, preformatted=True) - - yield Ok(first_line) - - # --- Middle (String) Line - # We only need to yield one (possibly too long) string line, since the - # `StringSplitter` will break it down further if necessary. - string_value = LL[string_idx].value - string_line = Line( - mode=line.mode, - depth=line.depth + 1, - inside_brackets=True, - should_split_rhs=line.should_split_rhs, - magic_trailing_comma=line.magic_trailing_comma, - ) - string_leaf = Leaf(token.STRING, string_value) - insert_str_child(string_leaf) - string_line.append(string_leaf) - - old_rpar_leaf = None - if is_valid_index(string_idx + 1): - right_leaves = LL[string_idx + 1 :] - if ends_with_comma: - right_leaves.pop() - - if old_parens_exist: - assert ( - right_leaves and right_leaves[-1].type == token.RPAR - ), "Apparently, old parentheses do NOT exist?!" - old_rpar_leaf = right_leaves.pop() - - append_leaves(string_line, line, right_leaves) - - yield Ok(string_line) - - # --- Last Line - last_line = line.clone() - last_line.bracket_tracker = first_line.bracket_tracker - - new_rpar_leaf = Leaf(token.RPAR, ")") - if old_rpar_leaf is not None: - replace_child(old_rpar_leaf, new_rpar_leaf) - else: - insert_str_child(new_rpar_leaf) - last_line.append(new_rpar_leaf) - - # If the target string ended with a comma, we place this comma to the - # right of the RPAR on the last line. - if ends_with_comma: - comma_leaf = Leaf(token.COMMA, ",") - replace_child(LL[comma_idx], comma_leaf) - last_line.append(comma_leaf) - - yield Ok(last_line) - - -class StringParser: - """ - A state machine that aids in parsing a string's "trailer", which can be - either non-existent, an old-style formatting sequence (e.g. `% varX` or `% - (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX, - varY)`). - - NOTE: A new StringParser object MUST be instantiated for each string - trailer we need to parse. - - Examples: - We shall assume that `line` equals the `Line` object that corresponds - to the following line of python code: - ``` - x = "Some {}.".format("String") + some_other_string - ``` - - Furthermore, we will assume that `string_idx` is some index such that: - ``` - assert line.leaves[string_idx].value == "Some {}." - ``` - - The following code snippet then holds: - ``` - string_parser = StringParser() - idx = string_parser.parse(line.leaves, string_idx) - assert line.leaves[idx].type == token.PLUS - ``` - """ - - DEFAULT_TOKEN = -1 - - # String Parser States - START = 1 - DOT = 2 - NAME = 3 - PERCENT = 4 - SINGLE_FMT_ARG = 5 - LPAR = 6 - RPAR = 7 - DONE = 8 - - # Lookup Table for Next State - _goto: Dict[Tuple[ParserState, NodeType], ParserState] = { - # A string trailer may start with '.' OR '%'. - (START, token.DOT): DOT, - (START, token.PERCENT): PERCENT, - (START, DEFAULT_TOKEN): DONE, - # A '.' MUST be followed by an attribute or method name. - (DOT, token.NAME): NAME, - # A method name MUST be followed by an '(', whereas an attribute name - # is the last symbol in the string trailer. - (NAME, token.LPAR): LPAR, - (NAME, DEFAULT_TOKEN): DONE, - # A '%' symbol can be followed by an '(' or a single argument (e.g. a - # string or variable name). - (PERCENT, token.LPAR): LPAR, - (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG, - # If a '%' symbol is followed by a single argument, that argument is - # the last leaf in the string trailer. - (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE, - # If present, a ')' symbol is the last symbol in a string trailer. - # (NOTE: LPARS and nested RPARS are not included in this lookup table, - # since they are treated as a special case by the parsing logic in this - # classes' implementation.) - (RPAR, DEFAULT_TOKEN): DONE, - } - - def __init__(self) -> None: - self._state = self.START - self._unmatched_lpars = 0 - - def parse(self, leaves: List[Leaf], string_idx: int) -> int: - """ - Pre-conditions: - * @leaves[@string_idx].type == token.STRING - - Returns: - The index directly after the last leaf which is apart of the string - trailer, if a "trailer" exists. - OR - @string_idx + 1, if no string "trailer" exists. - """ - assert leaves[string_idx].type == token.STRING - - idx = string_idx + 1 - while idx < len(leaves) and self._next_state(leaves[idx]): - idx += 1 - return idx - - 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]`). - * On the next call to this function, the leaf parameter passed in - MUST be the leaf directly following @leaf. - - Returns: - True iff @leaf is apart of the string's trailer. - """ - # We ignore empty LPAR or RPAR leaves. - if is_empty_par(leaf): - return True - - next_token = leaf.type - if next_token == token.LPAR: - self._unmatched_lpars += 1 - - current_state = self._state - - # The LPAR parser state is a special case. We will return True until we - # find the matching RPAR token. - if current_state == self.LPAR: - if next_token == token.RPAR: - self._unmatched_lpars -= 1 - if self._unmatched_lpars == 0: - self._state = self.RPAR - # Otherwise, we use a lookup table to determine the next state. - else: - # If the lookup table matches the current state to the next - # token, we use the lookup table. - if (current_state, next_token) in self._goto: - self._state = self._goto[current_state, next_token] - else: - # Otherwise, we check if a the current state was assigned a - # default. - if (current_state, self.DEFAULT_TOKEN) in self._goto: - self._state = self._goto[current_state, self.DEFAULT_TOKEN] - # If no default has been assigned, then this parser has a logic - # error. - else: - raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!") - - if self._state == self.DONE: - return False - - return True - - -def TErr(err_msg: str) -> Err[CannotTransform]: - """(T)ransform Err - - Convenience function used when working with the TResult type. - """ - cant_transform = CannotTransform(err_msg) - return Err(cant_transform) - - -def contains_pragma_comment(comment_list: List[Leaf]) -> bool: - """ - Returns: - True iff one of the comments in @comment_list is a pragma used by one - of the more common static analysis tools for python (e.g. mypy, flake8, - pylint). - """ - for comment in comment_list: - if comment.value.startswith(("# type:", "# noqa", "# pylint:")): - return True - - return False - - -def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]: - """ - Factory for a convenience function that is used to orphan @string_leaf - and then insert multiple new leaves into the same part of the node - structure that @string_leaf had originally occupied. - - Examples: - Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N = - string_leaf.parent`. Assume the node `N` has the following - original structure: - - Node( - expr_stmt, [ - Leaf(NAME, 'x'), - Leaf(EQUAL, '='), - Leaf(STRING, '"foo"'), - ] - ) - - We then run the code snippet shown below. - ``` - insert_str_child = insert_str_child_factory(string_leaf) - - lpar = Leaf(token.LPAR, '(') - insert_str_child(lpar) - - bar = Leaf(token.STRING, '"bar"') - insert_str_child(bar) - - rpar = Leaf(token.RPAR, ')') - insert_str_child(rpar) - ``` - - After which point, it follows that `string_leaf.parent is None` and - the node `N` now has the following structure: - - Node( - expr_stmt, [ - Leaf(NAME, 'x'), - Leaf(EQUAL, '='), - Leaf(LPAR, '('), - Leaf(STRING, '"bar"'), - Leaf(RPAR, ')'), - ] - ) - """ - string_parent = string_leaf.parent - string_child_idx = string_leaf.remove() - - def insert_str_child(child: LN) -> None: - nonlocal string_child_idx - - assert string_parent is not None - assert string_child_idx is not None - - string_parent.insert_child(string_child_idx, child) - string_child_idx += 1 - - return insert_str_child - - -def has_triple_quotes(string: str) -> bool: - """ - Returns: - True iff @string starts with three quotation characters. - """ - raw_string = string.lstrip(STRING_PREFIX_CHARS) - return raw_string[:3] in {'"""', "'''"} - - -def parent_type(node: Optional[LN]) -> Optional[NodeType]: - """ - Returns: - @node.parent.type, if @node is not None and has a parent. - OR - None, otherwise. - """ - if node is None or node.parent is None: - return None - - return node.parent.type - - -def is_empty_par(leaf: Leaf) -> bool: - return is_empty_lpar(leaf) or is_empty_rpar(leaf) - - -def is_empty_lpar(leaf: Leaf) -> bool: - return leaf.type == token.LPAR and leaf.value == "" - - -def is_empty_rpar(leaf: Leaf) -> bool: - return leaf.type == token.RPAR and leaf.value == "" - - -def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]: - """ - Examples: - ``` - my_list = [1, 2, 3] - - is_valid_index = is_valid_index_factory(my_list) - - assert is_valid_index(0) - assert is_valid_index(2) - - assert not is_valid_index(3) - assert not is_valid_index(-1) - ``` - """ - - def is_valid_index(idx: int) -> bool: - """ - Returns: - True iff @idx is positive AND seq[@idx] does NOT raise an - IndexError. - """ - return 0 <= idx < len(seq) - - return is_valid_index - - -def line_to_string(line: Line) -> str: - """Returns the string representation of @line. - - WARNING: This is known to be computationally expensive. - """ - return str(line).strip("\n") - - -def append_leaves( - new_line: Line, old_line: Line, leaves: List[Leaf], preformatted: bool = False -) -> None: - """ - Append leaves (taken from @old_line) to @new_line, making sure to fix the - underlying Node structure where appropriate. - - All of the leaves in @leaves are duplicated. The duplicates are then - appended to @new_line and used to replace their originals in the underlying - Node structure. Any comments attached to the old leaves are reattached to - the new leaves. - - Pre-conditions: - set(@leaves) is a subset of set(@old_line.leaves). - """ - for old_leaf in leaves: - new_leaf = Leaf(old_leaf.type, old_leaf.value) - replace_child(old_leaf, new_leaf) - new_line.append(new_leaf, preformatted=preformatted) - - for comment_leaf in old_line.comments_after(old_leaf): - new_line.append(comment_leaf, preformatted=True) - - -def replace_child(old_child: LN, new_child: LN) -> None: - """ - Side Effects: - * If @old_child.parent is set, replace @old_child with @new_child in - @old_child's underlying Node structure. - OR - * Otherwise, this function does nothing. - """ - parent = old_child.parent - if not parent: - return - - child_idx = old_child.remove() - if child_idx is not None: - parent.insert_child(child_idx, new_child) - - -def get_string_prefix(string: str) -> str: - """ - Pre-conditions: - * assert_is_leaf_string(@string) - - Returns: - @string's prefix (e.g. '', 'r', 'f', or 'rf'). - """ - assert_is_leaf_string(string) - - prefix = "" - prefix_idx = 0 - while string[prefix_idx] in STRING_PREFIX_CHARS: - prefix += string[prefix_idx].lower() - prefix_idx += 1 - - return prefix - - -def assert_is_leaf_string(string: str) -> None: - """ - Checks the pre-condition that @string has the format that you would expect - of `leaf.value` where `leaf` is some Leaf such that `leaf.type == - token.STRING`. A more precise description of the pre-conditions that are - checked are listed below. - - Pre-conditions: - * @string starts with either ', ", ', or " where - `set()` is some subset of `set(STRING_PREFIX_CHARS)`. - * @string ends with a quote character (' or "). - - Raises: - AssertionError(...) if the pre-conditions listed above are not - satisfied. - """ - dquote_idx = string.find('"') - squote_idx = string.find("'") - if -1 in [dquote_idx, squote_idx]: - quote_idx = max(dquote_idx, squote_idx) - else: - quote_idx = min(squote_idx, dquote_idx) - - assert ( - 0 <= quote_idx < len(string) - 1 - ), f"{string!r} is missing a starting quote character (' or \")." - assert string[-1] in ( - "'", - '"', - ), f"{string!r} is missing an ending quote character (' or \")." - assert set(string[:quote_idx]).issubset( - set(STRING_PREFIX_CHARS) - ), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}." - - -def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> 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. - Prefer RHS otherwise. This is why this function is not symmetrical with - :func:`right_hand_split` which also handles optional parentheses. - """ - tail_leaves: List[Leaf] = [] - body_leaves: List[Leaf] = [] - head_leaves: List[Leaf] = [] - current_leaves = head_leaves - matching_bracket: Optional[Leaf] = None - for leaf in line.leaves: - if ( - current_leaves is body_leaves - and leaf.type in CLOSING_BRACKETS - and leaf.opening_bracket is matching_bracket - ): - current_leaves = tail_leaves if body_leaves else head_leaves - current_leaves.append(leaf) - if current_leaves is head_leaves: - if leaf.type in OPENING_BRACKETS: - matching_bracket = leaf - current_leaves = body_leaves - 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) - bracket_split_succeeded_or_raise(head, body, tail) - for result in (head, body, tail): - if result: - yield result - - -def right_hand_split( - line: Line, - line_length: int, - features: Collection[Feature] = (), - omit: Collection[LeafID] = (), -) -> Iterator[Line]: - """Split line into many lines, starting with the last matching bracket pair. - - If the split was by optional parentheses, attempt splitting without them, too. - `omit` is a collection of closing bracket IDs that shouldn't be considered for - this split. - - Note: running this function modifies `bracket_depth` on the leaves of `line`. - """ - tail_leaves: List[Leaf] = [] - body_leaves: List[Leaf] = [] - head_leaves: List[Leaf] = [] - current_leaves = tail_leaves - opening_bracket: Optional[Leaf] = None - closing_bracket: Optional[Leaf] = None - for leaf in reversed(line.leaves): - if current_leaves is body_leaves: - if leaf is opening_bracket: - current_leaves = head_leaves if body_leaves else tail_leaves - current_leaves.append(leaf) - if current_leaves is tail_leaves: - if leaf.type in CLOSING_BRACKETS and id(leaf) not in omit: - opening_bracket = leaf.opening_bracket - closing_bracket = leaf - current_leaves = body_leaves - if not (opening_bracket and closing_bracket and head_leaves): - # If there is no opening or closing_bracket that means the split failed and - # all content is in the tail. Otherwise, if `head_leaves` are empty, it means - # the matching `opening_bracket` wasn't available on `line` anymore. - raise CannotSplit("No brackets found") - - 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) - bracket_split_succeeded_or_raise(head, body, tail) - 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 - # the closing bracket is an optional paren - and closing_bracket.type == token.RPAR - and not 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 we can actually remove the parens - and can_omit_invisible_parens(body, line_length, omit_on_explode=omit) - ): - omit = {id(closing_bracket), *omit} - try: - yield from right_hand_split(line, line_length, features=features, omit=omit) - return - - except CannotSplit: - 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." - ) - - elif head.contains_multiline_strings() or 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" - " contains multiline strings which by definition never fit one" - " line." - ) - - ensure_visible(opening_bracket) - ensure_visible(closing_bracket) - for result in (head, body, tail): - if result: - yield result - - -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. - - Do nothing otherwise. - - A left- or right-hand split is based on a pair of brackets. Content before - (and including) the opening bracket is left on one line, content inside the - brackets is put on a separate line, and finally content starting with and - following the closing bracket is put on a separate line. - - Those are called `head`, `body`, and `tail`, respectively. If the split - produced the same line (all content in `head`) or ended up with an empty `body` - and the `tail` is just the closing bracket, then it's considered failed. - """ - tail_len = len(str(tail).strip()) - if not body: - if tail_len == 0: - raise CannotSplit("Splitting brackets produced the same line") - - elif tail_len < 3: - raise CannotSplit( - f"Splitting brackets on an empty body to save {tail_len} characters is" - " not worth it" - ) - - -def bracket_split_build_line( - leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False -) -> Line: - """Return a new line with given `leaves` and respective comments from `original`. - - 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. - """ - result = Line(mode=original.mode, depth=original.depth) - if is_body: - 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 = ( - original.is_def - and opening_bracket.value == "(" - and not any(leaf.type == token.COMMA for leaf in leaves) - ) - - if original.is_import or no_commas: - for i in range(len(leaves) - 1, -1, -1): - if leaves[i].type == STANDALONE_COMMENT: - continue - - if leaves[i].type != token.COMMA: - new_comma = Leaf(token.COMMA, ",") - leaves.insert(i + 1, new_comma) - break - - # Populate the line - for leaf in leaves: - result.append(leaf, preformatted=True) - for comment_after in original.comments_after(leaf): - result.append(comment_after, preformatted=True) - if is_body and should_split_line(result, opening_bracket): - result.should_split_rhs = True - return result - - -def dont_increase_indentation(split_func: Transformer) -> Transformer: - """Normalize prefix of the first leaf in every line returned by `split_func`. - - This is a decorator over relevant split functions. - """ - - @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 - - return split_wrapper - - -@dont_increase_indentation -def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: - """Split according to delimiters of the highest priority. - - If the appropriate Features are given, the split will add trailing commas - also in function signatures and calls that contain `*` and `**`. - """ - try: - last_leaf = line.leaves[-1] - except IndexError: - raise CannotSplit("Line empty") - - bt = line.bracket_tracker - try: - delimiter_priority = bt.max_delimiter_priority(exclude={id(last_leaf)}) - except ValueError: - raise CannotSplit("No delimiters found") - - if delimiter_priority == DOT_PRIORITY: - if bt.delimiter_count_with_priority(delimiter_priority) == 1: - raise CannotSplit("Splitting a single attribute from its owner looks wrong") - - current_line = Line( - mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets - ) - lowest_depth = sys.maxsize - trailing_comma_safe = True - - def append_to_line(leaf: Leaf) -> Iterator[Line]: - """Append `leaf` to current line or to new line if appending impossible.""" - nonlocal current_line - try: - current_line.append_safe(leaf, preformatted=True) - except ValueError: - yield current_line - - current_line = Line( - mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets - ) - current_line.append(leaf) - - for leaf in line.leaves: - yield from append_to_line(leaf) - - for comment_after in line.comments_after(leaf): - yield from append_to_line(comment_after) - - lowest_depth = min(lowest_depth, leaf.bracket_depth) - if leaf.bracket_depth == lowest_depth: - if is_vararg(leaf, within={syms.typedargslist}): - trailing_comma_safe = ( - trailing_comma_safe and Feature.TRAILING_COMMA_IN_DEF in features - ) - elif is_vararg(leaf, within={syms.arglist, syms.argument}): - trailing_comma_safe = ( - trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features - ) - - leaf_priority = bt.delimiters.get(id(leaf)) - if leaf_priority == delimiter_priority: - yield current_line - - current_line = 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) - yield current_line - - -@dont_increase_indentation -def standalone_comment_split( - line: Line, features: Collection[Feature] = () -) -> Iterator[Line]: - """Split standalone comments from the rest of the line.""" - if not line.contains_standalone_comments(0): - raise CannotSplit("Line does not have any standalone comments") - - current_line = Line( - mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets - ) - - def append_to_line(leaf: Leaf) -> Iterator[Line]: - """Append `leaf` to current line or to new line if appending impossible.""" - nonlocal current_line - try: - current_line.append_safe(leaf, preformatted=True) - except ValueError: - yield current_line - - current_line = Line( - line.mode, depth=line.depth, inside_brackets=line.inside_brackets - ) - current_line.append(leaf) - - for leaf in line.leaves: - yield from append_to_line(leaf) - - for comment_after in line.comments_after(leaf): - yield from append_to_line(comment_after) - - if current_line: - yield current_line - - -def is_import(leaf: Leaf) -> bool: - """Return True if the given leaf starts an import statement.""" - p = leaf.parent - t = leaf.type - v = leaf.value - return bool( - t == token.NAME - and ( - (v == "import" and p and p.type == syms.import_name) - or (v == "from" and p and p.type == syms.import_from) - ) - ) - - -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.""" - t = leaf.type - v = leaf.value - return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix) - - -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_string_prefix(leaf: Leaf, remove_u_prefix: bool = False) -> None: - """Make all string prefixes lowercase. - - If remove_u_prefix is given, also removes any u prefix from the string. - - Note: Mutates its argument. - """ - match = re.match(r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", leaf.value, re.DOTALL) - assert match is not None, f"failed to match string {leaf.value!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", "") - leaf.value = f"{new_prefix}{match.group(2)}" - - -def normalize_string_quotes(leaf: Leaf) -> None: - """Prefer double quotes but only if it doesn't cause more escaping. - - Adds or removes backslashes as appropriate. Doesn't parse and fix - strings nested in f-strings (yet). - - Note: Mutates its argument. - """ - value = leaf.value.lstrip(STRING_PREFIX_CHARS) - if value[:3] == '"""': - return - - elif value[:3] == "'''": - orig_quote = "'''" - new_quote = '"""' - elif value[0] == '"': - orig_quote = '"' - new_quote = "'" - else: - orig_quote = "'" - new_quote = '"' - first_quote_pos = leaf.value.find(orig_quote) - if first_quote_pos == -1: - return # There's an internal error - - prefix = leaf.value[: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}") - body = leaf.value[first_quote_pos + len(orig_quote) : -len(orig_quote)] - if "r" in prefix.casefold(): - if unescaped_new_quote.search(body): - # There's at least one unescaped new_quote in this raw string - # so converting is impossible - return - - # Do not introduce or remove backslashes in raw strings - new_body = body - else: - # remove unnecessary escapes - new_body = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", body) - if body != new_body: - # Consider the string without unnecessary escapes as the original - body = new_body - leaf.value = f"{prefix}{orig_quote}{body}{orig_quote}" - new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body) - new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body) - if "f" in prefix.casefold(): - matches = re.findall( - r""" - (?:[^{]|^)\{ # start of the string or a non-{ followed by a single { - ([^{].*?) # contents of the brackets except if begins with {{ - \}(?:[^}]|$) # A } followed by end of the string or a non-} - """, - new_body, - re.VERBOSE, - ) - for m in matches: - if "\\" in str(m): - # Do not introduce backslashes in interpolated expressions - return - - if new_quote == '"""' and new_body[-1:] == '"': - # edge case: - new_body = new_body[:-1] + '\\"' - orig_escape_count = body.count("\\") - new_escape_count = new_body.count("\\") - if new_escape_count > orig_escape_count: - return # Do not introduce more escaping - - if new_escape_count == orig_escape_count and orig_quote == '"': - return # Prefer double quotes - - leaf.value = f"{prefix}{new_quote}{new_body}{new_quote}" - - -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). - """ - text = leaf.value.lower() - if text.startswith(("0o", "0b")): - # Leave octal and binary literals alone. - pass - elif text.startswith("0x"): - 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) - else: - text = format_float_or_int_string(text) - leaf.value = text - - -def format_hex(text: str) -> str: - """ - Formats a hexadecimal string like "0x12B3" - """ - before, after = text[:2], text[2:] - return f"{before}{after.upper()}" - - -def format_scientific_notation(text: str) -> str: - """Formats a numeric string utilizing scentific notation""" - before, after = text.split("e") - sign = "" - if after.startswith("-"): - after = after[1:] - sign = "-" - elif after.startswith("+"): - after = after[1:] - before = format_float_or_int_string(before) - 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`""" - 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}" - - -def format_float_or_int_string(text: str) -> str: - """Formats a float string like "1.0".""" - if "." not in text: - return text - - before, after = text.split(".") - return f"{before or 0}.{after or 0}" - - -def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: - """Make existing optional parentheses invisible or create new ones. - - `parens_after` is a set of string leaf values immediately after which parens - should be put. - - 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): - if pc.value in FMT_OFF: - # This `node` has a prefix with `# fmt: off`, don't mess with parens. - return - 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) - - # Add parentheses around long tuple unpacking in assignments. - if ( - index == 0 - and isinstance(child, Node) - and child.type == syms.testlist_star_expr - ): - check_lpar = True - - if check_lpar: - if child.type == syms.atom: - if maybe_make_parens_invisible_in_atom(child, parent=node): - wrap_in_parentheses(node, child, visible=False) - 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 child.type == token.LPAR: - # make parentheses invisible - child.value = "" # type: ignore - node.children[-1].value = "" # type: ignore - elif child.type != token.STAR: - # insert invisible parentheses - node.insert_child(index, Leaf(token.LPAR, "")) - node.append_child(Leaf(token.RPAR, "")) - break - - 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 - - -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) - - -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): - if comment.value not in FMT_PASS: - 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: - 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: - continue - - ignored_nodes = list(generate_ignored_nodes(leaf, comment)) - if not ignored_nodes: - continue - - first = ignored_nodes[0] # Can be a container node with the `leaf`. - parent = first.parent - prefix = first.prefix - first.prefix = prefix[comment.consumed :] - 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: - hidden_value += " " + 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). - hidden_value = hidden_value[:-1] - first_idx: Optional[int] = None - for ignored in ignored_nodes: - index = ignored.remove() - if first_idx is None: - first_idx = index - assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" - assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" - parent.insert_child( - first_idx, - Leaf( - STANDALONE_COMMENT, - hidden_value, - prefix=prefix[:previous_consumed] + "\n" * comment.newlines, - ), - ) - return True - - return False - - -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. - """ - 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, "") - 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 - return - while container is not None and container.type != token.ENDMARKER: - if is_fmt_on(container): - return - - # fix for fmt: on in children - if contains_fmt_on_at_column(container, leaf.column): - for child in container.children: - if contains_fmt_on_at_column(child, leaf.column): - return - yield child - else: - yield container - container = container.next_sibling - - -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): - if comment.value in FMT_ON: - fmt_on = True - elif comment.value in FMT_OFF: - fmt_on = False - return fmt_on - - -def contains_fmt_on_at_column(container: LN, column: int) -> bool: - """Determine if children at a given column 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): - return True - - return False - - -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 maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> 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 ( - 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 - ): - return False - - if is_walrus_assignment(node): - if parent.type in [ - syms.annassign, - syms.expr_stmt, - syms.assert_stmt, - syms.return_stmt, - # these ones aren't useful to end users, but they do please fuzzers - syms.for_stmt, - syms.del_stmt, - ]: - return False - - first = node.children[0] - last = node.children[-1] - if first.type == token.LPAR and last.type == token.RPAR: - middle = node.children[1] - # make parentheses invisible - first.value = "" # type: ignore - last.value = "" # type: ignore - maybe_make_parens_invisible_in_atom(middle, parent=parent) - - if is_atom_with_invisible_parens(middle): - # Strip the invisible parens from `middle` by replacing - # it with the child in-between the invisible parens - middle.replace(middle.children[1]) - - return False - - return True - - -def is_atom_with_invisible_parens(node: LN) -> bool: - """Given a `LN`, determines whether it's an atom `node` with invisible - parens. Useful in dedupe-ing and normalizing parens. - """ - if isinstance(node, Leaf) or node.type != syms.atom: - return False - - first, last = node.children[0], node.children[-1] - return ( - isinstance(first, Leaf) - and first.type == token.LPAR - and first.value == "" - and isinstance(last, Leaf) - and last.type == token.RPAR - and last.value == "" - ) - - -def is_empty_tuple(node: LN) -> bool: - """Return True if `node` holds an empty tuple.""" - return ( - node.type == syms.atom - and len(node.children) == 2 - and node.children[0].type == token.LPAR - and node.children[1].type == token.RPAR - ) - - -def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: - """Returns `wrapped` if `node` is of the shape ( wrapped ). - - Parenthesis can be optional. Returns None otherwise""" - if len(node.children) != 3: - return None - - lpar, wrapped, rpar = node.children - if not (lpar.type == token.LPAR and rpar.type == token.RPAR): - return None - - return wrapped - - -def first_child_is_arith(node: Node) -> bool: - """Whether first child is an arithmetic or a binary arithmetic expression""" - expr_types = { - 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 wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: - """Wrap `child` in parentheses. - - This replaces `child` with an atom holding the parentheses and the old - child. That requires moving the prefix. - - If `visible` is False, the leaves will be valueless (and thus invisible). - """ - lpar = Leaf(token.LPAR, "(" if visible else "") - rpar = Leaf(token.RPAR, ")" if visible else "") - prefix = child.prefix - child.prefix = "" - index = child.remove() or 0 - new_child = Node(syms.atom, [lpar, child, rpar]) - new_child.prefix = prefix - parent.insert_child(index, new_child) - - -def is_one_tuple(node: LN) -> bool: - """Return True if `node` holds a tuple with one element, with or without parens.""" - if node.type == syms.atom: - gexp = unwrap_singleton_parenthesis(node) - if gexp is None or gexp.type != syms.testlist_gexp: - return False - - return len(gexp.children) == 2 and gexp.children[1].type == token.COMMA - - return ( - node.type in IMPLICIT_TUPLE - and len(node.children) == 2 - and node.children[1].type == token.COMMA - ) - - -def is_walrus_assignment(node: LN) -> bool: - """Return True iff `node` is of the shape ( test := test )""" - inner = unwrap_singleton_parenthesis(node) - return inner is not None and inner.type == syms.namedexpr_test - - -def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: - """Return True iff `node` is a trailer valid in a simple decorator""" - return node.type == syms.trailer and ( - ( - len(node.children) == 2 - and node.children[0].type == token.DOT - and node.children[1].type == token.NAME - ) - # last trailer can be an argument-less parentheses pair - or ( - last - and len(node.children) == 2 - and node.children[0].type == token.LPAR - and node.children[1].type == token.RPAR - ) - # last trailer can be arguments - or ( - last - and len(node.children) == 3 - and node.children[0].type == token.LPAR - # and node.children[1].type == syms.argument - and node.children[2].type == token.RPAR - ) - ) - - -def is_simple_decorator_expression(node: LN) -> bool: - """Return True iff `node` could be a 'dotted name' decorator - - This function takes the node of the 'namedexpr_test' of the new decorator - grammar and test if it would be valid under the old decorator grammar. - - The old grammar was: decorator: @ dotted_name [arguments] NEWLINE - The new grammar is : decorator: @ namedexpr_test NEWLINE - """ - if node.type == token.NAME: - return True - if node.type == syms.power: - if node.children: - return ( - node.children[0].type == token.NAME - and all(map(is_simple_decorator_trailer, node.children[1:-1])) - and ( - len(node.children) < 2 - or is_simple_decorator_trailer(node.children[-1], last=True) - ) - ) - return False - - -def is_yield(node: LN) -> bool: - """Return True if `node` holds a `yield` or `yield from` expression.""" - if node.type == syms.yield_expr: - return True - - if node.type == token.NAME and node.value == "yield": # type: ignore - return True - - if node.type != syms.atom: - return False - - if len(node.children) != 3: - return False - - lpar, expr, rpar = node.children - if lpar.type == token.LPAR and rpar.type == token.RPAR: - return is_yield(expr) - - return False - - -def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool: - """Return True if `leaf` is a star or double star in a vararg or kwarg. - - If `within` includes VARARGS_PARENTS, this applies to function signatures. - If `within` includes UNPACKING_PARENTS, it applies to right hand-side - extended iterable unpacking (PEP 3132) and additional unpacking - generalizations (PEP 448). - """ - if leaf.type not in VARARGS_SPECIALS or not leaf.parent: - return False - - p = leaf.parent - if p.type == syms.star_expr: - # Star expressions are also used as assignment targets in extended - # iterable unpacking (PEP 3132). See what its parent is instead. - if not p.parent: - return False - - p = p.parent - - return p.type in within - - -def is_multiline_string(leaf: Leaf) -> bool: - """Return True if `leaf` is a multiline string that actually spans many lines.""" - return has_triple_quotes(leaf.value) and "\n" in leaf.value - - -def is_stub_suite(node: Node) -> bool: - """Return True if `node` is a suite with a stub body.""" - if ( - len(node.children) != 4 - or node.children[0].type != token.NEWLINE - or node.children[1].type != token.INDENT - or node.children[3].type != token.DEDENT - ): - return False - - return is_stub_body(node.children[2]) - - -def is_stub_body(node: LN) -> bool: - """Return True if `node` is a simple statement containing an ellipsis.""" - if not isinstance(node, Node) or node.type != syms.simple_stmt: - return False - - if len(node.children) != 2: - return False - - child = node.children[0] - return ( - child.type == syms.atom - and len(child.children) == 3 - and all(leaf == Leaf(token.DOT, ".") for leaf in child.children) - ) - - -def max_delimiter_priority_in_atom(node: LN) -> Priority: - """Return maximum delimiter priority inside `node`. - - This is specific to atoms with contents contained in a pair of parentheses. - If `node` isn't an atom or there are no enclosing parentheses, returns 0. - """ - if node.type != syms.atom: - return 0 - - first = node.children[0] - last = node.children[-1] - if not (first.type == token.LPAR and last.type == token.RPAR): - return 0 - - bt = BracketTracker() - for c in node.children[1:-1]: - if isinstance(c, Leaf): - bt.mark(c) - else: - for leaf in c.leaves(): - bt.mark(leaf) - try: - return bt.max_delimiter_priority() - - except ValueError: - return 0 - - -def ensure_visible(leaf: Leaf) -> None: - """Make sure parentheses are visible. - - They could be invisible as part of some statements (see - :func:`normalize_invisible_parens` and :func:`visit_import_from`). - """ - if leaf.type == token.LPAR: - leaf.value = "(" - elif leaf.type == token.RPAR: - leaf.value = ")" - - -def should_split_line(line: Line, opening_bracket: Leaf) -> bool: - """Should `line` be immediately split with `delimiter_split()` after RHS?""" - - if not (opening_bracket.parent and opening_bracket.value in "[{("): - return False - - # We're essentially checking if the body is delimited by commas and there's more - # than one of them (we're excluding the trailing comma and if the delimiter priority - # is still commas, that means there's more). - exclude = set() - trailing_comma = False - try: - last_leaf = line.leaves[-1] - if last_leaf.type == token.COMMA: - trailing_comma = True - exclude.add(id(last_leaf)) - max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) - except (IndexError, ValueError): - return False - - return max_priority == COMMA_PRIORITY and ( - (line.mode.magic_trailing_comma and trailing_comma) - # always explode imports - or opening_bracket.parent.type in {syms.atom, syms.import_from} - ) - - -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: - return False - - depth = closing.bracket_depth + 1 - for _opening_index, leaf in enumerate(leaves): - if leaf is opening: - break - - else: - raise LookupError("Opening paren not found in `leaves`") - - commas = 0 - _opening_index += 1 - for leaf in leaves[_opening_index:]: - if leaf is closing: - break - - bracket_depth = leaf.bracket_depth - if bracket_depth == depth and leaf.type == token.COMMA: - commas += 1 - if leaf.parent and leaf.parent.type in { - syms.arglist, - syms.typedargslist, - }: - commas += 1 - break - - return commas < 2 - - -def get_features_used(node: Node) -> Set[Feature]: - """Return a set of (relatively) new Python features used in this file. - - Currently looking for: - - f-strings; - - underscores in numeric literals; - - trailing commas after * or ** in function signatures and calls; - - positional only arguments in function signatures and lambdas; - - assignment expression; - - relaxed decorator syntax; - """ - features: Set[Feature] = set() - for n in node.pre_order(): - if n.type == token.STRING: - value_head = n.value[:2] # type: ignore - if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}: - features.add(Feature.F_STRINGS) - - elif n.type == token.NUMBER: - if "_" in n.value: # type: ignore - features.add(Feature.NUMERIC_UNDERSCORES) - - elif n.type == token.SLASH: - if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}: - features.add(Feature.POS_ONLY_ARGUMENTS) - - elif n.type == token.COLONEQUAL: - features.add(Feature.ASSIGNMENT_EXPRESSIONS) - - elif n.type == syms.decorator: - if len(n.children) > 1 and not is_simple_decorator_expression( - n.children[1] - ): - features.add(Feature.RELAXED_DECORATORS) - - elif ( - n.type in {syms.typedargslist, syms.arglist} - and n.children - and n.children[-1].type == token.COMMA - ): - if n.type == syms.typedargslist: - feature = Feature.TRAILING_COMMA_IN_DEF - else: - feature = Feature.TRAILING_COMMA_IN_CALL - - for ch in n.children: - if ch.type in STARS: - features.add(feature) - - if ch.type == syms.argument: - for argch in ch.children: - if argch.type in STARS: - features.add(feature) - - return features - - -def detect_target_versions(node: Node) -> Set[TargetVersion]: - """Detect the version to target based on the nodes used.""" - features = get_features_used(node) - return { - version for version in TargetVersion if features <= VERSION_TO_FEATURES[version] - } - - -def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]: - """Generate sets of closing bracket IDs that should be omitted in a RHS. - - Brackets can be omitted if the entire trailer up to and including - a preceding closing bracket fits in one line. - - Yielded sets are cumulative (contain results of previous yields, too). First - set is empty, unless the line should explode, in which case bracket pairs until - the one that needs to explode are omitted. - """ - - omit: Set[LeafID] = set() - if not line.magic_trailing_comma: - yield omit - - length = 4 * line.depth - opening_bracket: Optional[Leaf] = None - closing_bracket: Optional[Leaf] = None - inner_brackets: Set[LeafID] = set() - for index, leaf, leaf_length in enumerate_with_length(line, reversed=True): - length += leaf_length - if length > line_length: - break - - has_inline_comment = leaf_length > len(leaf.value) + len(leaf.prefix) - if leaf.type == STANDALONE_COMMENT or has_inline_comment: - break - - if opening_bracket: - if leaf is opening_bracket: - opening_bracket = None - elif leaf.type in CLOSING_BRACKETS: - prev = line.leaves[index - 1] if index > 0 else None - if ( - prev - and prev.type == token.COMMA - and not is_one_tuple_between( - leaf.opening_bracket, leaf, line.leaves - ) - ): - # Never omit bracket pairs with trailing commas. - # We need to explode on those. - break - - inner_brackets.add(id(leaf)) - elif leaf.type in CLOSING_BRACKETS: - prev = line.leaves[index - 1] if index > 0 else None - if prev and prev.type in OPENING_BRACKETS: - # Empty brackets would fail a split so treat them as "inner" - # brackets (e.g. only add them to the `omit` set if another - # pair of brackets was good enough. - inner_brackets.add(id(leaf)) - continue - - if closing_bracket: - omit.add(id(closing_bracket)) - omit.update(inner_brackets) - inner_brackets.clear() - yield omit - - if ( - prev - and prev.type == token.COMMA - and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) - ): - # Never omit bracket pairs with trailing commas. - # We need to explode on those. - break - - if leaf.value: - opening_bracket = leaf.opening_bracket - closing_bracket = leaf - - -def get_future_imports(node: Node) -> Set[str]: - """Return a set of __future__ imports in the file.""" - imports: Set[str] = set() - - def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]: - for child in children: - if isinstance(child, Leaf): - if child.type == token.NAME: - yield child.value - - elif child.type == syms.import_as_name: - orig_name = child.children[0] - assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports" - assert orig_name.type == token.NAME, "Invalid syntax parsing imports" - yield orig_name.value - - elif child.type == syms.import_as_names: - yield from get_imports_from_children(child.children) - - else: - raise AssertionError("Invalid syntax parsing imports") - - for child in node.children: - if child.type != syms.simple_stmt: - break - - first_child = child.children[0] - if isinstance(first_child, Leaf): - # Continue looking if we see a docstring; otherwise stop. - if ( - len(child.children) == 2 - and first_child.type == token.STRING - and child.children[1].type == token.NEWLINE - ): - continue - - break - - elif first_child.type == syms.import_from: - module_name = first_child.children[1] - if not isinstance(module_name, Leaf) or module_name.value != "__future__": - break - - imports |= set(get_imports_from_children(first_child.children[3:])) - else: - break - - return imports - - -@lru_cache() -def get_gitignore(root: Path) -> PathSpec: - """Return a PathSpec matching gitignore content if present.""" - gitignore = root / ".gitignore" - lines: List[str] = [] - if gitignore.is_file(): - with gitignore.open() as gf: - lines = gf.readlines() - return PathSpec.from_lines("gitwildmatch", lines) - - -def normalize_path_maybe_ignore( - path: Path, root: Path, report: "Report" -) -> Optional[str]: - """Normalize `path`. May return `None` if `path` was ignored. - - `report` is where "path ignored" output goes. - """ - try: - 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}") - return None - - except ValueError: - if path.is_symlink(): - report.path_ignored(path, f"is a symbolic link that points outside {root}") - return None - - raise - - return normalized_path - - -def path_is_excluded( - normalized_path: str, - pattern: Optional[Pattern[str]], -) -> bool: - match = pattern.search(normalized_path) if pattern else None - return bool(match and match.group(0)) - - -def gen_python_files( - paths: Iterable[Path], - root: Path, - include: Pattern[str], - exclude: Pattern[str], - extend_exclude: Optional[Pattern[str]], - force_exclude: Optional[Pattern[str]], - report: "Report", - gitignore: Optional[PathSpec], -) -> Iterator[Path]: - """Generate all files under `path` whose paths are not excluded by the - `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, - but are included by the `include` regex. - - Symbolic links pointing outside of the `root` directory are ignored. - - `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) - if normalized_path is None: - 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") - continue - - # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. - normalized_path = "/" + normalized_path - if child.is_dir(): - normalized_path += "/" - - if path_is_excluded(normalized_path, exclude): - report.path_ignored(child, "matches the --exclude regular expression") - continue - - if path_is_excluded(normalized_path, extend_exclude): - report.path_ignored( - child, "matches the --extend-exclude regular expression" - ) - continue - - if path_is_excluded(normalized_path, force_exclude): - report.path_ignored(child, "matches the --force-exclude regular expression") - continue - - if child.is_dir(): - yield from gen_python_files( - child.iterdir(), - root, - include, - exclude, - extend_exclude, - force_exclude, - report, - gitignore, - ) - - elif child.is_file(): - include_match = include.search(normalized_path) if include else True - if include_match: - yield child - - -@lru_cache() -def find_project_root(srcs: Tuple[str, ...]) -> Path: - """Return a directory containing .git, .hg, or pyproject.toml. - - That directory will be a common parent of all files and directories - passed in `srcs`. - - 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. - """ - if not srcs: - return Path("/").resolve() - - path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] - - # A list of lists of parents for each 'src'. 'src' is included as a - # "parent" of itself if it is a directory - src_parents = [ - list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs - ] - - common_base = max( - set.intersection(*(set(parents) for parents in src_parents)), - key=lambda path: path.parts, - ) - - for directory in (common_base, *common_base.parents): - if (directory / ".git").exists(): - return directory - - if (directory / ".hg").is_dir(): - return directory - - if (directory / "pyproject.toml").is_file(): - return directory - - return directory - - -@lru_cache() -def find_user_pyproject_toml() -> Path: - r"""Return the path to the top-level user configuration for black. - - This looks for ~\.black on Windows and ~/.config/black on Linux and other - Unix systems. - """ - if sys.platform == "win32": - # Windows - user_config_path = Path.home() / ".black" - else: - config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config") - user_config_path = Path(config_root).expanduser() / "black" - return user_config_path.resolve() - - -@dataclass -class Report: - """Provides a reformatting counter. Can be rendered with `str(report)`.""" - - check: bool = False - diff: bool = False - quiet: bool = False - verbose: bool = False - change_count: int = 0 - same_count: int = 0 - failure_count: int = 0 - - def done(self, src: Path, changed: Changed) -> None: - """Increment the counter for successful reformatting. Write out a message.""" - if changed is Changed.YES: - reformatted = "would reformat" if self.check or self.diff else "reformatted" - if self.verbose or not self.quiet: - out(f"{reformatted} {src}") - self.change_count += 1 - else: - if self.verbose: - if changed is Changed.NO: - msg = f"{src} already well formatted, good job." - else: - msg = f"{src} wasn't modified on disk since last run." - out(msg, bold=False) - self.same_count += 1 - - def failed(self, src: Path, message: str) -> None: - """Increment the counter for failed reformatting. Write out a message.""" - err(f"error: cannot format {src}: {message}") - self.failure_count += 1 - - def path_ignored(self, path: Path, message: str) -> None: - if self.verbose: - out(f"{path} ignored: {message}", bold=False) - - @property - def return_code(self) -> int: - """Return the exit code that the app should use. - - This considers the current state of changed files and failures: - - if there were any failures, return 123; - - if any files were changed and --check is being used, return 1; - - otherwise return 0. - """ - # According to http://tldp.org/LDP/abs/html/exitcodes.html starting with - # 126 we have special return codes reserved by the shell. - if self.failure_count: - return 123 - - elif self.change_count and self.check: - return 1 - - return 0 - - def __str__(self) -> str: - """Render a color report of the current state. - - Use `click.unstyle` to remove colors. - """ - if self.check or self.diff: - reformatted = "would be reformatted" - unchanged = "would be left unchanged" - failed = "would fail to reformat" - else: - reformatted = "reformatted" - unchanged = "left unchanged" - failed = "failed to reformat" - report = [] - if self.change_count: - s = "s" if self.change_count > 1 else "" - report.append( - click.style(f"{self.change_count} file{s} {reformatted}", bold=True) - ) - if self.same_count: - s = "s" if self.same_count > 1 else "" - report.append(f"{self.same_count} file{s} {unchanged}") - if self.failure_count: - s = "s" if self.failure_count > 1 else "" - report.append( - click.style(f"{self.failure_count} file{s} {failed}", fg="red") - ) - return ", ".join(report) + "." - - -def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: - filename = "" - if sys.version_info >= (3, 8): - # TODO: support Python 4+ ;) - for minor_version in range(sys.version_info[1], 4, -1): - try: - return ast.parse(src, filename, feature_version=(3, minor_version)) - except SyntaxError: - continue - else: - for feature_version in (7, 6): - try: - return ast3.parse(src, filename, feature_version=feature_version) - except SyntaxError: - continue - if ast27.__name__ == "ast": - raise SyntaxError( - "The requested source code has invalid Python 3 syntax.\n" - "If you are trying to format Python 2 files please reinstall Black" - " with the 'python2' extra: `python3 -m pip install black[python2]`." - ) - return ast27.parse(src) - - -def _fixup_ast_constants( - node: Union[ast.AST, ast3.AST, ast27.AST] -) -> Union[ast.AST, ast3.AST, ast27.AST]: - """Map ast nodes deprecated in 3.8 to Constant.""" - if isinstance(node, (ast.Str, ast3.Str, ast27.Str, ast.Bytes, ast3.Bytes)): - return ast.Constant(value=node.s) - - if isinstance(node, (ast.Num, ast3.Num, ast27.Num)): - return ast.Constant(value=node.n) - - if isinstance(node, (ast.NameConstant, ast3.NameConstant)): - return ast.Constant(value=node.value) - - return node - - -def _stringify_ast( - node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0 -) -> Iterator[str]: - """Simple visitor generating strings to compare ASTs by content.""" - - node = _fixup_ast_constants(node) - - 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 - - try: - value = getattr(node, field) - except AttributeError: - continue - - yield f"{' ' * (depth+1)}{field}=" - - if isinstance(value, list): - for item in value: - # Ignore nested tuples within del statements, because we may insert - # 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)) - ): - for item in item.elts: - yield from _stringify_ast(item, depth + 2) - - 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)): - 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)) - ): - 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() - else: - normalized = value - yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" - - yield f"{' ' * depth}) # /{node.__class__.__name__}" - - -def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None: - """Raise AssertionError if `src` and `dst` aren't equivalent.""" - try: - 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}" - ) - - try: - dst_ast = parse_ast(dst) - 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}. " - "Please report a bug on https://github.com/psf/black/issues. " - f"This invalid output might be helpful: {log}" - ) from None - - src_ast_str = "\n".join(_stringify_ast(src_ast)) - dst_ast_str = "\n".join(_stringify_ast(dst_ast)) - if src_ast_str != dst_ast_str: - 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"https://github.com/psf/black/issues. This diff might be helpful: {log}" - ) from 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) - if dst != newdst: - log = dump_to_file( - str(mode), - diff(src, dst, "source", "first pass"), - diff(dst, newdst, "first pass", "second pass"), - ) - raise AssertionError( - "INTERNAL ERROR: Black produced different code on the second pass of the" - " formatter. Please report a bug on https://github.com/psf/black/issues." - f" This diff might be helpful: {log}" - ) from None - - -@mypyc_attr(patchable=True) -def dump_to_file(*output: str, ensure_final_newline: bool = True) -> str: - """Dump `output` to a temporary file. Return path to the file.""" - with tempfile.NamedTemporaryFile( - mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8" - ) as f: - for lines in output: - f.write(lines) - if ensure_final_newline and lines and lines[-1] != "\n": - f.write("\n") - return f.name - - -@contextmanager -def nullcontext() -> Iterator[None]: - """Return an empty context manager. - - To be used like `nullcontext` in Python 3.7. - """ - yield - - -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)] - diff_lines = [] - for line in difflib.unified_diff( - a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 - ): - # Work around https://bugs.python.org/issue2142 - # See https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html - if line[-1] == "\n": - diff_lines.append(line) - else: - diff_lines.append(line + "\n") - diff_lines.append("\\ No newline at end of file\n") - return "".join(diff_lines) - - -def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: - """asyncio signal handler that cancels all `tasks` and reports to stderr.""" - err("Aborted!") - for task in tasks: - task.cancel() - - -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()] - if not to_cancel: - return - - for task in to_cancel: - task.cancel() - 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. - # Silence their logger's spew about the event loop being closed. - cf_logger = logging.getLogger("concurrent.futures") - cf_logger.setLevel(logging.CRITICAL) - loop.close() - - -def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: - """Replace `regex` with `replacement` twice on `original`. - - This is used by string normalization to perform replaces on - overlapping matches. - """ - return regex.sub(replacement, regex.sub(replacement, original)) - - -def re_compile_maybe_verbose(regex: str) -> Pattern[str]: - """Compile a regular expression string in `regex`. - - If it contains newlines, use verbose mode. - """ - if "\n" in regex: - regex = "(?x)" + regex - compiled: Pattern[str] = re.compile(regex) - return compiled - - -def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]: - """Like `reversed(enumerate(sequence))` if that were possible.""" - index = len(sequence) - 1 - for element in reversed(sequence): - yield (index, element) - index -= 1 - - -def enumerate_with_length( - line: Line, reversed: bool = False -) -> Iterator[Tuple[Index, Leaf, int]]: - """Return an enumeration of leaves with their length. - - Stops prematurely on multiline strings and standalone comments. - """ - op = cast( - Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]], - enumerate_reversed if reversed else enumerate, - ) - for index, leaf in op(line.leaves): - length = len(leaf.prefix) + len(leaf.value) - if "\n" in leaf.value: - return # Multiline strings, we can't continue. - - for comment in line.comments_after(leaf): - length += len(comment.value) - - yield index, leaf, length - - -def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool: - """Return True if `line` is no longer than `line_length`. - - 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() - ) - - -def can_be_split(line: Line) -> bool: - """Return False if the line cannot be split *for sure*. - - This is not an exhaustive search but a cheap heuristic that we can use to - avoid some unfortunate formattings (mostly around wrapping unsplittable code - in unnecessary parentheses). - """ - leaves = line.leaves - if len(leaves) < 2: - return False - - if leaves[0].type == token.STRING and leaves[1].type == token.DOT: - call_count = 0 - dot_count = 0 - next = leaves[-1] - for leaf in leaves[-2::-1]: - if leaf.type in OPENING_BRACKETS: - if next.type not in CLOSING_BRACKETS: - return False - - call_count += 1 - elif leaf.type == token.DOT: - dot_count += 1 - elif leaf.type == token.NAME: - if not (next.type == token.DOT or next.type in OPENING_BRACKETS): - return False - - elif leaf.type not in CLOSING_BRACKETS: - return False - - if dot_count > 1 and call_count > 1: - return False - - return True - - -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? - - 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. - """ - 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: - # With more than one delimiter of a kind the optional parentheses read better. - return False - - if max_priority == DOT_PRIORITY: - # A single stranded method call doesn't require optional parentheses. - return True - - assert len(line.leaves) >= 2, "Stranded delimiter" - - # With a single delimiter, omit if the expression starts or ends with - # a bracket. - first = line.leaves[0] - second = line.leaves[1] - if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS: - if _can_omit_opening_paren(line, first=first, line_length=line_length): - return True - - # Note: we are not returning False here because a line might have *both* - # a leading opening bracket and a trailing closing bracket. If the - # opening bracket doesn't match our rule, maybe the closing will. - - 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 - or last.type == token.RBRACE - or ( - # don't use indexing for omitting optional parentheses; - # it looks weird - last.type == token.RSQB - and last.parent - and last.parent.type != syms.trailer - ) - ): - if penultimate.type in OPENING_BRACKETS: - # Empty brackets don't help. - return False - - if is_multiline_string(first): - # Additional wrapping of a multiline string in this situation is - # 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 - - return False - - -def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool: - """See `can_omit_invisible_parens`.""" - remainder = False - length = 4 * line.depth - _index = -1 - for _index, leaf, leaf_length in enumerate_with_length(line): - if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: - remainder = True - if remainder: - length += leaf_length - if length > line_length: - break - - if leaf.type in OPENING_BRACKETS: - # There are brackets we can further split on. - remainder = False - - else: - # checked the entire string and line length wasn't exceeded - if len(line.leaves) == _index + 1: - return True - - return False - - -def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool: - """See `can_omit_invisible_parens`.""" - length = 4 * line.depth - seen_other_brackets = False - for _index, leaf, leaf_length in enumerate_with_length(line): - length += leaf_length - if leaf is last.opening_bracket: - if seen_other_brackets or length <= line_length: - return True - - elif leaf.type in OPENING_BRACKETS: - # There are brackets we can further split on. - seen_other_brackets = True - - return False - - -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 - 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 run_transformer( - line: Line, - transform: Transformer, - mode: Mode, - features: Collection[Feature], - *, - line_str: str = "", -) -> List[Line]: - if not line_str: - line_str = line_to_string(line) - result: List[Line] = [] - for transformed_line in transform(line, features): - if str(transformed_line).strip("\n") == line_str: - raise CannotTransform("Line transformer returned an unchanged result") - - result.extend(transform_line(transformed_line, mode=mode, features=features)) - - if not ( - transform.__name__ == "rhs" - and line.bracket_tracker.invisible - and not any(bracket.value for bracket in line.bracket_tracker.invisible) - and not line.contains_multiline_strings() - and not result[0].contains_uncollapsable_type_comments() - and not result[0].contains_unsplittable_type_ignore() - and not is_line_short_enough(result[0], line_length=mode.line_length) - ): - return result - - line_copy = line.clone() - append_leaves(line_copy, line, line.leaves) - features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} - 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 - ): - result = second_opinion - return result - - -def get_cache_file(mode: Mode) -> Path: - return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle" - - -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): - return {} - - return cache - - -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 - - -def patch_click() -> None: - """Make Click not crash. - - 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. - """ - try: - from click import core - from click import _unicodefun # type: ignore - except ModuleNotFoundError: - return - - for module in (core, _unicodefun): - if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None + for module in (core, _unicodefun): + if hasattr(module, "_verify_python3_env"): + module._verify_python3_env = lambda: None def patched_main() -> None: @@ -7025,66 +1056,5 @@ def patched_main() -> None: main() -def is_docstring(leaf: Leaf) -> bool: - if prev_siblings_are( - leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] - ): - return True - - # Multiline docstring on the same line as the `def`. - if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]): - # `syms.parameters` is only used in funcdefs and async_funcdefs in the Python - # grammar. We're safe to return True without further checks. - return True - - return False - - -def lines_with_leading_tabs_expanded(s: str) -> List[str]: - """ - Splits string into lines and expands only leading tabs (following the normal - Python rules) - """ - lines = [] - 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) - if match: - first_non_whitespace_idx = match.start(1) - - lines.append( - line[:first_non_whitespace_idx].expandtabs() - + line[first_non_whitespace_idx:] - ) - else: - lines.append(line) - return lines - - -def fix_docstring(docstring: str, prefix: str) -> str: - # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation - if not docstring: - return "" - lines = lines_with_leading_tabs_expanded(docstring) - # Determine minimum indentation (first line doesn't count): - indent = sys.maxsize - for line in lines[1:]: - stripped = line.lstrip() - if stripped: - indent = min(indent, len(line) - len(stripped)) - # Remove indentation (first line is special): - trimmed = [lines[0].strip()] - if indent < sys.maxsize: - last_line_idx = len(lines) - 2 - for i, line in enumerate(lines[1:]): - stripped_line = line[indent:].rstrip() - if stripped_line or i == last_line_idx: - trimmed.append(prefix + stripped_line) - else: - trimmed.append("") - return "\n".join(trimmed) - - if __name__ == "__main__": patched_main() diff --git a/src/black/brackets.py b/src/black/brackets.py new file mode 100644 index 00000000000..bb865a0d5b7 --- /dev/null +++ b/src/black/brackets.py @@ -0,0 +1,334 @@ +"""Builds on top of nodes.py to track brackets.""" + +from dataclasses import dataclass, field +import sys +from typing import Dict, Iterable, List, Optional, Tuple, Union + +if sys.version_info < (3, 8): + from typing_extensions import Final +else: + from typing import Final + +from blib2to3.pytree import Leaf, Node +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 + +# types +LN = Union[Leaf, Node] +Depth = int +LeafID = int +NodeType = int +Priority = int + + +COMPREHENSION_PRIORITY: Final = 20 +COMMA_PRIORITY: Final = 18 +TERNARY_PRIORITY: Final = 16 +LOGIC_PRIORITY: Final = 14 +STRING_PRIORITY: Final = 12 +COMPARATOR_PRIORITY: Final = 10 +MATH_PRIORITIES: Final = { + token.VBAR: 9, + token.CIRCUMFLEX: 8, + token.AMPER: 7, + token.LEFTSHIFT: 6, + token.RIGHTSHIFT: 6, + token.PLUS: 5, + token.MINUS: 5, + token.STAR: 4, + token.SLASH: 4, + token.DOUBLESLASH: 4, + token.PERCENT: 4, + token.AT: 4, + token.TILDE: 3, + token.DOUBLESTAR: 2, +} +DOT_PRIORITY: Final = 1 + + +class BracketMatchError(KeyError): + """Raised when an opening bracket is unable to be matched to a closing bracket.""" + + +@dataclass +class BracketTracker: + """Keeps track of brackets on a line.""" + + depth: int = 0 + bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict) + delimiters: Dict[LeafID, Priority] = field(default_factory=dict) + previous: Optional[Leaf] = None + _for_loop_depths: List[int] = field(default_factory=list) + _lambda_argument_depths: List[int] = field(default_factory=list) + invisible: List[Leaf] = field(default_factory=list) + + def mark(self, leaf: Leaf) -> None: + """Mark `leaf` with bracket-related metadata. Keep track of delimiters. + + All leaves receive an int `bracket_depth` field that stores how deep + within brackets a given leaf is. 0 means there are no enclosing brackets + 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 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 + `delimiters` field. + """ + if leaf.type == token.COMMENT: + return + + self.maybe_decrement_after_for_loop_variable(leaf) + self.maybe_decrement_after_lambda_arguments(leaf) + if leaf.type in CLOSING_BRACKETS: + self.depth -= 1 + try: + opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) + except KeyError as e: + raise BracketMatchError( + "Unable to match a closing bracket to the following opening" + f" bracket: {leaf}" + ) from e + leaf.opening_bracket = opening_bracket + if not leaf.value: + self.invisible.append(leaf) + leaf.bracket_depth = self.depth + if self.depth == 0: + delim = is_split_before_delimiter(leaf, self.previous) + if delim and self.previous is not None: + self.delimiters[id(self.previous)] = delim + else: + delim = is_split_after_delimiter(leaf, self.previous) + if delim: + self.delimiters[id(leaf)] = delim + if leaf.type in OPENING_BRACKETS: + self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf + self.depth += 1 + if not leaf.value: + self.invisible.append(leaf) + self.previous = leaf + self.maybe_increment_lambda_arguments(leaf) + self.maybe_increment_for_loop_variable(leaf) + + def any_open_brackets(self) -> bool: + """Return True if there is an yet unmatched open bracket on the line.""" + return bool(self.bracket_match) + + def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: + """Return the highest priority of a delimiter found on the line. + + Values are consistent with what `is_split_*_delimiter()` return. + Raises ValueError on no delimiters. + """ + return max(v for k, v in self.delimiters.items() if k not in exclude) + + def delimiter_count_with_priority(self, priority: Priority = 0) -> int: + """Return the number of delimiters with the given `priority`. + + If no `priority` is passed, defaults to max priority on the line. + """ + if not self.delimiters: + return 0 + + priority = priority or self.max_delimiter_priority() + return sum(1 for p in self.delimiters.values() if p == priority) + + def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool: + """In a for loop, or comprehension, the variables are often unpacks. + + To avoid splitting on the comma in this situation, increase the depth of + tokens between `for` and `in`. + """ + if leaf.type == token.NAME and leaf.value == "for": + self.depth += 1 + self._for_loop_depths.append(self.depth) + return True + + return False + + def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool: + """See `maybe_increment_for_loop_variable` above for explanation.""" + if ( + self._for_loop_depths + and self._for_loop_depths[-1] == self.depth + and leaf.type == token.NAME + and leaf.value == "in" + ): + self.depth -= 1 + self._for_loop_depths.pop() + return True + + return False + + def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool: + """In a lambda expression, there might be more than one argument. + + To avoid splitting on the comma in this situation, increase the depth of + tokens between `lambda` and `:`. + """ + if leaf.type == token.NAME and leaf.value == "lambda": + self.depth += 1 + self._lambda_argument_depths.append(self.depth) + return True + + return False + + def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool: + """See `maybe_increment_lambda_arguments` above for explanation.""" + if ( + self._lambda_argument_depths + and self._lambda_argument_depths[-1] == self.depth + and leaf.type == token.COLON + ): + self.depth -= 1 + self._lambda_argument_depths.pop() + return True + + return False + + def get_open_lsqb(self) -> Optional[Leaf]: + """Return the most recent opening square bracket (if any).""" + return self.bracket_match.get((self.depth - 1, token.RSQB)) + + +def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: + """Return the priority of the `leaf` delimiter, given a line break after it. + + The delimiter priorities returned here are from those delimiters that would + cause a line break after themselves. + + Higher numbers are higher priority. + """ + if leaf.type == token.COMMA: + return COMMA_PRIORITY + + return 0 + + +def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: + """Return the priority of the `leaf` delimiter, given a line break before it. + + The delimiter priorities returned here are from those delimiters that would + cause a line break before themselves. + + Higher numbers are higher priority. + """ + if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS): + # * and ** might also be MATH_OPERATORS but in this case they are not. + # Don't treat them as a delimiter. + return 0 + + if ( + leaf.type == token.DOT + and leaf.parent + and leaf.parent.type not in {syms.import_from, syms.dotted_name} + and (previous is None or previous.type in CLOSING_BRACKETS) + ): + return DOT_PRIORITY + + if ( + leaf.type in MATH_OPERATORS + and leaf.parent + and leaf.parent.type not in {syms.factor, syms.star_expr} + ): + return MATH_PRIORITIES[leaf.type] + + if leaf.type in COMPARATORS: + return COMPARATOR_PRIORITY + + if ( + leaf.type == token.STRING + and previous is not None + and previous.type == token.STRING + ): + return STRING_PRIORITY + + if leaf.type not in {token.NAME, token.ASYNC}: + return 0 + + if ( + leaf.value == "for" + and leaf.parent + and leaf.parent.type in {syms.comp_for, syms.old_comp_for} + or leaf.type == token.ASYNC + ): + if ( + not isinstance(leaf.prev_sibling, Leaf) + or leaf.prev_sibling.value != "async" + ): + return COMPREHENSION_PRIORITY + + if ( + leaf.value == "if" + and leaf.parent + and leaf.parent.type in {syms.comp_if, syms.old_comp_if} + ): + return COMPREHENSION_PRIORITY + + if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test: + return TERNARY_PRIORITY + + if leaf.value == "is": + return COMPARATOR_PRIORITY + + if ( + leaf.value == "in" + and leaf.parent + and leaf.parent.type in {syms.comp_op, syms.comparison} + and not ( + previous is not None + and previous.type == token.NAME + and previous.value == "not" + ) + ): + return COMPARATOR_PRIORITY + + if ( + leaf.value == "not" + and leaf.parent + and leaf.parent.type == syms.comp_op + and not ( + previous is not None + and previous.type == token.NAME + and previous.value == "is" + ) + ): + return COMPARATOR_PRIORITY + + if leaf.value in LOGIC_OPERATORS and leaf.parent: + return LOGIC_PRIORITY + + return 0 + + +def max_delimiter_priority_in_atom(node: LN) -> Priority: + """Return maximum delimiter priority inside `node`. + + This is specific to atoms with contents contained in a pair of parentheses. + If `node` isn't an atom or there are no enclosing parentheses, returns 0. + """ + if node.type != syms.atom: + return 0 + + first = node.children[0] + last = node.children[-1] + if not (first.type == token.LPAR and last.type == token.RPAR): + return 0 + + bt = BracketTracker() + for c in node.children[1:-1]: + if isinstance(c, Leaf): + bt.mark(c) + else: + for leaf in c.leaves(): + bt.mark(leaf) + try: + return bt.max_delimiter_priority() + + except ValueError: + return 0 diff --git a/src/black/cache.py b/src/black/cache.py new file mode 100644 index 00000000000..017a2a91362 --- /dev/null +++ b/src/black/cache.py @@ -0,0 +1,83 @@ +"""Caching of formatted files with feature-based invalidation.""" + +import os +import pickle +from pathlib import Path +import tempfile +from typing import Dict, Iterable, Set, Tuple + +from appdirs import user_cache_dir + +from black.mode import Mode + +from _black_version import version as __version__ + + +# types +Timestamp = float +FileSize = int +CacheInfo = Tuple[Timestamp, FileSize] +Cache = Dict[str, CacheInfo] + + +CACHE_DIR = Path(user_cache_dir("black", version=__version__)) + + +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): + 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 diff --git a/src/black/comments.py b/src/black/comments.py new file mode 100644 index 00000000000..415e391b2a7 --- /dev/null +++ b/src/black/comments.py @@ -0,0 +1,269 @@ +from dataclasses import dataclass +from functools import lru_cache +import regex as re +from typing import Iterator, List, Optional, Union + +from blib2to3.pytree import Node, Leaf +from blib2to3.pgen2 import token + +from black.nodes import first_leaf_column, preceding_leaf, container_of +from black.nodes import STANDALONE_COMMENT, WHITESPACE + +# 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"} + + +@dataclass +class ProtoComment: + """Describes a piece of syntax that is a comment. + + It's not a :class:`blib2to3.pytree.Leaf` so that: + + * it can be cached (`Leaf` objects should not be reused more than once as + they store their lineno, column, prefix, and parent information); + * `newlines` and `consumed` fields are kept separate from the `value`. This + simplifies handling of special marker comments like ``# fmt: off/on``. + """ + + type: int # token.COMMENT or STANDALONE_COMMENT + 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 + + +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 + in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation + move because it does away with modifying the grammar to include all the + possible places in which comments can be placed. + + The sad consequence for us though is that comments don't "belong" anywhere. + This is why this function generates simple parentless Leaf objects for + comments. We simply don't know what the correct parent should be. + + No matter though, we can live without this. We really only need to + differentiate between inline and standalone comments. The latter don't + share the line with any code. + + 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): + yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) + + +@lru_cache(maxsize=4096) +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: + return result + + 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() + if not line: + nlines += 1 + 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 + # as a simple trailing comment. + if line.endswith("\\"): + ignored_lines += 1 + continue + + if index == ignored_lines and not is_endmarker: + comment_type = token.COMMENT # simple trailing comment + else: + comment_type = STANDALONE_COMMENT + comment = make_comment(line) + result.append( + ProtoComment( + type=comment_type, value=comment, newlines=nlines, consumed=consumed + ) + ) + nlines = 0 + return result + + +def make_comment(content: str) -> str: + """Return a consistently formatted comment from the given `content` string. + + All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single + space between the hash sign and the content. + + If `content` didn't start with a hash sign, one is provided. + """ + content = content.rstrip() + if not content: + return "#" + + if content[0] == "#": + content = content[1:] + NON_BREAKING_SPACE = " " + if ( + content + and content[0] == NON_BREAKING_SPACE + and not content.lstrip().startswith("type:") + ): + content = " " + content[1:] # Replace NBSP by a simple space + if content and content[0] not in " !:#'%": + content = " " + content + return "#" + content + + +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) + + +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): + if comment.value not in FMT_PASS: + 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: + 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: + continue + + ignored_nodes = list(generate_ignored_nodes(leaf, comment)) + if not ignored_nodes: + continue + + first = ignored_nodes[0] # Can be a container node with the `leaf`. + parent = first.parent + prefix = first.prefix + first.prefix = prefix[comment.consumed :] + 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: + hidden_value += " " + 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). + hidden_value = hidden_value[:-1] + first_idx: Optional[int] = None + for ignored in ignored_nodes: + index = ignored.remove() + if first_idx is None: + first_idx = index + assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" + assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" + parent.insert_child( + first_idx, + Leaf( + STANDALONE_COMMENT, + hidden_value, + prefix=prefix[:previous_consumed] + "\n" * comment.newlines, + ), + ) + return True + + return False + + +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. + """ + 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, "") + 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 + return + while container is not None and container.type != token.ENDMARKER: + if is_fmt_on(container): + return + + # fix for fmt: on in children + if contains_fmt_on_at_column(container, leaf.column): + for child in container.children: + if contains_fmt_on_at_column(child, leaf.column): + return + yield child + else: + yield container + container = container.next_sibling + + +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): + if comment.value in FMT_ON: + fmt_on = True + elif comment.value in FMT_OFF: + fmt_on = False + return fmt_on + + +def contains_fmt_on_at_column(container: LN, column: int) -> bool: + """Determine if children at a given column 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): + return True + + return False + + +def contains_pragma_comment(comment_list: List[Leaf]) -> bool: + """ + Returns: + True iff one of the comments in @comment_list is a pragma used by one + of the more common static analysis tools for python (e.g. mypy, flake8, + pylint). + """ + for comment in comment_list: + if comment.value.startswith(("# type:", "# noqa", "# pylint:")): + return True + + return False diff --git a/src/black/concurrency.py b/src/black/concurrency.py new file mode 100644 index 00000000000..119a9a71faf --- /dev/null +++ b/src/black/concurrency.py @@ -0,0 +1,39 @@ +import asyncio +import logging +import sys +from typing import Any, Iterable + +from black.output import err + + +def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: + """asyncio signal handler that cancels all `tasks` and reports to stderr.""" + err("Aborted!") + for task in tasks: + task.cancel() + + +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()] + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + 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. + # Silence their logger's spew about the event loop being closed. + cf_logger = logging.getLogger("concurrent.futures") + cf_logger.setLevel(logging.CRITICAL) + loop.close() diff --git a/src/black/const.py b/src/black/const.py new file mode 100644 index 00000000000..821258588ab --- /dev/null +++ b/src/black/const.py @@ -0,0 +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_INCLUDES = r"\.pyi?$" +STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" diff --git a/src/black/debug.py b/src/black/debug.py new file mode 100644 index 00000000000..5143076ab35 --- /dev/null +++ b/src/black/debug.py @@ -0,0 +1,48 @@ +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 + +LN = Union[Leaf, Node] +T = TypeVar("T") + + +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + 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.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) + else: + _type = token.tok_name.get(node.type, str(node.type)) + 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) + + @classmethod + def show(cls, code: Union[str, Leaf, Node]) -> None: + """Pretty-print the lib2to3 AST of a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + if isinstance(code, str): + code = lib2to3_parse(code) + list(v.visit(code)) diff --git a/src/black/files.py b/src/black/files.py new file mode 100644 index 00000000000..1be560643a1 --- /dev/null +++ b/src/black/files.py @@ -0,0 +1,241 @@ +from functools import lru_cache +import io +import os +from pathlib import Path +import sys +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + Optional, + Pattern, + Sequence, + Tuple, + Union, + TYPE_CHECKING, +) + +from pathspec import PathSpec +import toml + +from black.output import err +from black.report import Report + +if TYPE_CHECKING: + import colorama # noqa: F401 + + +@lru_cache() +def find_project_root(srcs: Sequence[str]) -> Path: + """Return a directory containing .git, .hg, or pyproject.toml. + + That directory will be a common parent of all files and directories + passed in `srcs`. + + 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. + """ + if not srcs: + return Path("/").resolve() + + path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] + + # A list of lists of parents for each 'src'. 'src' is included as a + # "parent" of itself if it is a directory + src_parents = [ + list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs + ] + + common_base = max( + set.intersection(*(set(parents) for parents in src_parents)), + key=lambda path: path.parts, + ) + + for directory in (common_base, *common_base.parents): + if (directory / ".git").exists(): + return directory + + if (directory / ".hg").is_dir(): + return directory + + if (directory / "pyproject.toml").is_file(): + return directory + + return directory + + +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_pyproject_toml = path_project_root / "pyproject.toml" + if path_pyproject_toml.is_file(): + return str(path_pyproject_toml) + + try: + path_user_pyproject_toml = find_user_pyproject_toml() + return ( + str(path_user_pyproject_toml) + if path_user_pyproject_toml.is_file() + else None + ) + except PermissionError 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 + + +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 toml.TomlDecodeError + """ + pyproject_toml = toml.load(path_config) + config = pyproject_toml.get("tool", {}).get("black", {}) + return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} + + +@lru_cache() +def find_user_pyproject_toml() -> Path: + r"""Return the path to the top-level user configuration for black. + + This looks for ~\.black on Windows and ~/.config/black on Linux and other + Unix systems. + """ + if sys.platform == "win32": + # Windows + user_config_path = Path.home() / ".black" + else: + config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config") + user_config_path = Path(config_root).expanduser() / "black" + return user_config_path.resolve() + + +@lru_cache() +def get_gitignore(root: Path) -> PathSpec: + """Return a PathSpec matching gitignore content if present.""" + gitignore = root / ".gitignore" + lines: List[str] = [] + if gitignore.is_file(): + with gitignore.open() as gf: + lines = gf.readlines() + return PathSpec.from_lines("gitwildmatch", lines) + + +def normalize_path_maybe_ignore( + path: Path, root: Path, report: Report +) -> Optional[str]: + """Normalize `path`. May return `None` if `path` was ignored. + + `report` is where "path ignored" output goes. + """ + try: + 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}") + return None + + except ValueError: + if path.is_symlink(): + report.path_ignored(path, f"is a symbolic link that points outside {root}") + return None + + raise + + return normalized_path + + +def path_is_excluded( + normalized_path: str, + pattern: Optional[Pattern[str]], +) -> bool: + match = pattern.search(normalized_path) if pattern else None + return bool(match and match.group(0)) + + +def gen_python_files( + paths: Iterable[Path], + root: Path, + include: Pattern[str], + exclude: Pattern[str], + extend_exclude: Optional[Pattern[str]], + force_exclude: Optional[Pattern[str]], + report: Report, + gitignore: Optional[PathSpec], +) -> Iterator[Path]: + """Generate all files under `path` whose paths are not excluded by the + `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, + but are included by the `include` regex. + + Symbolic links pointing outside of the `root` directory are ignored. + + `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) + if normalized_path is None: + 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") + continue + + # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. + normalized_path = "/" + normalized_path + if child.is_dir(): + normalized_path += "/" + + if path_is_excluded(normalized_path, exclude): + report.path_ignored(child, "matches the --exclude regular expression") + continue + + if path_is_excluded(normalized_path, extend_exclude): + report.path_ignored( + child, "matches the --extend-exclude regular expression" + ) + continue + + if path_is_excluded(normalized_path, force_exclude): + report.path_ignored(child, "matches the --force-exclude regular expression") + continue + + if child.is_dir(): + yield from gen_python_files( + child.iterdir(), + root, + include, + exclude, + extend_exclude, + force_exclude, + report, + gitignore, + ) + + elif child.is_file(): + include_match = include.search(normalized_path) if include else True + if include_match: + yield child + + +def wrap_stream_for_windows( + f: io.TextIOWrapper, +) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]: + """ + Wrap stream with colorama's wrap_stream so colors are shown on Windows. + + If `colorama` is unavailable, the original stream is returned unmodified. + Otherwise, the `wrap_stream()` function determines whether the stream needs + to be wrapped for a Windows environment and will accordingly either return + an `AnsiToWin32` wrapper or the original stream. + """ + try: + from colorama.initialise import wrap_stream + except ImportError: + return f + else: + # Set `strip=False` to avoid needing to modify test_express_diff_with_color. + return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True) diff --git a/src/black/linegen.py b/src/black/linegen.py new file mode 100644 index 00000000000..2e16b6fde44 --- /dev/null +++ b/src/black/linegen.py @@ -0,0 +1,984 @@ +""" +Generating lines of code. +""" +from functools import partial, wraps +import sys +from typing import Collection, Iterator, List, Optional, Set, Union + +from dataclasses import dataclass, field + +from black.nodes import WHITESPACE, 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 is_docstring, is_empty_tuple, is_one_tuple, is_one_tuple_between +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 +from black.trans import StringSplitter, StringParenWrapper, StringParenStripper +from black.mode import Mode +from black.mode import Feature + +from blib2to3.pytree import Node, Leaf +from blib2to3.pgen2 import token + + +# types +LeafID = int +LN = Union[Leaf, Node] + + +class CannotSplit(CannotTransform): + """A readable split that fits the allotted line length is impossible.""" + + +@dataclass +class LineGenerator(Visitor[Line]): + """Generates reformatted Line objects. Empty lines are not emitted. + + Note: destroys the tree it's visiting by mutating prefixes of its leaves + in ways that will no longer stringify to valid Python code on the tree. + """ + + mode: Mode + remove_u_prefix: bool = False + current_line: Line = field(init=False) + + def line(self, indent: int = 0) -> Iterator[Line]: + """Generate a line. + + If the line is empty, only emit if it makes sense. + If the line is too long, split it first and then generate. + + If any lines were generated, set up a new current_line. + """ + if not self.current_line: + self.current_line.depth += indent + return # Line is empty, don't emit. Creating a new one unnecessary. + + complete_line = self.current_line + self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) + yield complete_line + + 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): + if any_open_brackets: + # any comment within brackets is subject to splitting + self.current_line.append(comment) + elif comment.type == token.COMMENT: + # regular trailing comment + self.current_line.append(comment) + yield from self.line() + + else: + # regular standalone comment + yield from self.line() + + self.current_line.append(comment) + yield from self.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_quotes(node.value) + if node.type == token.NUMBER: + normalize_numeric_literal(node) + if node.type not in WHITESPACE: + self.current_line.append(node) + yield from super().visit_default(node) + + def visit_INDENT(self, node: Leaf) -> Iterator[Line]: + """Increase indentation level, maybe yield a line.""" + # In blib2to3 INDENT never holds comments. + yield from self.line(+1) + yield from self.visit_default(node) + + def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: + """Decrease indentation level, maybe yield a line.""" + # The current line might still wait for trailing comments. At DEDENT time + # there won't be any (they would be prefixes on the preceding NEWLINE). + # Emit the line then. + yield from self.line() + + # While DEDENT has no value, its prefix may contain standalone comments + # that belong to the current indentation level. Get 'em. + yield from self.visit_default(node) + + # Finally, emit the dedent. + yield from self.line(-1) + + def visit_stmt( + self, node: Node, keywords: Set[str], parens: Set[str] + ) -> Iterator[Line]: + """Visit a statement. + + This implementation is shared for `if`, `while`, `for`, `try`, `except`, + `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. + + `parens` holds a set of string leaf values immediately after which + invisible parens should be put. + """ + normalize_invisible_parens(node, parens_after=parens) + for child in node.children: + if child.type == token.NAME and child.value in keywords: # type: ignore + yield from self.line() + + 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): + yield from self.visit(node.children[2]) + else: + yield from self.visit_default(node) + + 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) + 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): + yield from self.visit_default(node) + else: + yield from self.line(+1) + yield from self.visit_default(node) + yield from self.line(-1) + + else: + if ( + not self.mode.is_pyi + or not node.parent + or not is_stub_suite(node.parent) + ): + yield from self.line() + yield from self.visit_default(node) + + def visit_async_stmt(self, node: Node) -> Iterator[Line]: + """Visit `async def`, `async for`, `async with`.""" + yield from self.line() + + children = iter(node.children) + for child in children: + yield from self.visit(child) + + if child.type == token.ASYNC: + break + + internal_stmt = next(children) + for child in internal_stmt.children: + yield from self.visit(child) + + def visit_decorators(self, node: Node) -> Iterator[Line]: + """Visit decorators.""" + for child in node.children: + yield from self.line() + yield from self.visit(child) + + def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: + """Remove a semicolon and put the other statement on a separate line.""" + yield from self.line() + + def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]: + """End of file. Process outstanding comments and end with a newline.""" + yield from self.visit_default(leaf) + yield from self.line() + + def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: + if not self.current_line.bracket_tracker.any_open_brackets(): + yield from self.line() + yield from self.visit_default(leaf) + + def visit_factor(self, node: Node) -> Iterator[Line]: + """Force parentheses between a unary op and a binary power: + + -2 ** 8 -> -(2 ** 8) + """ + _operator, operand = node.children + if ( + operand.type == syms.power + and len(operand.children) == 3 + and operand.children[1].type == token.DOUBLESTAR + ): + lpar = Leaf(token.LPAR, "(") + rpar = Leaf(token.RPAR, ")") + index = operand.remove() or 0 + node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) + yield from self.visit_default(node) + + 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. + prefix = get_string_prefix(leaf.value) + docstring = leaf.value[len(prefix) :] # Remove the prefix + quote_char = docstring[0] + # A natural way to remove the outer quotes is to do: + # docstring = docstring.strip(quote_char) + # but that breaks on """""x""" (which is '""x'). + # So we actually need to remove the first character and the next two + # characters but only if they are the same as the first. + quote_len = 1 if docstring[1] != quote_char else 3 + docstring = docstring[quote_len:-quote_len] + + if is_multiline_string(leaf): + indent = " " * 4 * self.current_line.depth + docstring = fix_docstring(docstring, indent) + else: + docstring = docstring.strip() + + if docstring: + # Add some padding if the docstring starts / ends with a quote mark. + if docstring[0] == quote_char: + docstring = " " + docstring + if docstring[-1] == quote_char: + docstring += " " + if docstring[-1] == "\\": + backslash_count = len(docstring) - len(docstring.rstrip("\\")) + if backslash_count % 2: + # Odd number of tailing backslashes, add some padding to + # avoid escaping the closing string quote. + docstring += " " + else: + # Add some padding if the docstring is empty. + docstring = " " + + # We could enforce triple quotes at this point. + quote = quote_char * quote_len + leaf.value = prefix + quote + docstring + quote + + yield from self.visit_default(leaf) + + def __post_init__(self) -> None: + """You are in a twisty little maze of passages.""" + self.current_line = Line(mode=self.mode) + + v = self.visit_stmt + Ø: Set[str] = set() + self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","}) + self.visit_if_stmt = partial( + v, keywords={"if", "else", "elif"}, parens={"if", "elif"} + ) + self.visit_while_stmt = partial(v, keywords={"while", "else"}, parens={"while"}) + self.visit_for_stmt = partial(v, keywords={"for", "else"}, parens={"for", "in"}) + self.visit_try_stmt = partial( + v, keywords={"try", "except", "else", "finally"}, parens=Ø + ) + 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"}) + self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) + self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) + self.visit_async_funcdef = self.visit_async_stmt + self.visit_decorated = self.visit_decorators + + +def transform_line( + line: Line, mode: Mode, features: Collection[Feature] = () +) -> Iterator[Line]: + """Transform a `line`, potentially splitting it into many lines. + + They should fit in the allotted `line_length` but might not be able to. + + `features` are syntactical features that may be used in the output. + """ + if line.is_comment: + yield line + return + + line_str = line_to_string(line) + + ll = mode.line_length + sn = mode.string_normalization + string_merge = StringMerger(ll, sn) + string_paren_strip = StringParenStripper(ll, sn) + string_split = StringSplitter(ll, sn) + string_paren_wrap = StringParenWrapper(ll, sn) + + transformers: List[Transformer] + if ( + not line.contains_uncollapsable_type_comments() + 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) + or line.contains_unsplittable_type_ignore() + ) + 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: + transformers = [string_merge, string_paren_strip] + else: + transformers = [] + elif line.is_def: + transformers = [left_hand_split] + else: + + def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: + """Wraps calls to `right_hand_split`. + + The calls increasingly `omit` right-hand trailers (bracket pairs with + content), meaning the trailers get glued together to split on another + 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) + ) + # 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): + yield from lines + return + + # All splits failed, best effort split with no omits. + # 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 + ) + + if mode.experimental_string_processing: + if line.inside_brackets: + transformers = [ + string_merge, + string_paren_strip, + string_split, + delimiter_split, + standalone_comment_split, + string_paren_wrap, + rhs, + ] + else: + transformers = [ + string_merge, + string_paren_strip, + string_split, + string_paren_wrap, + rhs, + ] + else: + if line.inside_brackets: + transformers = [delimiter_split, standalone_comment_split, rhs] + else: + transformers = [rhs] + + for transform in transformers: + # We are accumulating lines in `result` because we might want to abort + # mission and return the original line in the end, or attempt a different + # split altogether. + try: + result = run_transformer(line, transform, mode, features, line_str=line_str) + except CannotTransform: + continue + else: + yield from result + break + + else: + yield line + + +def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> 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. + Prefer RHS otherwise. This is why this function is not symmetrical with + :func:`right_hand_split` which also handles optional parentheses. + """ + tail_leaves: List[Leaf] = [] + body_leaves: List[Leaf] = [] + head_leaves: List[Leaf] = [] + current_leaves = head_leaves + matching_bracket: Optional[Leaf] = None + for leaf in line.leaves: + if ( + current_leaves is body_leaves + and leaf.type in CLOSING_BRACKETS + and leaf.opening_bracket is matching_bracket + ): + current_leaves = tail_leaves if body_leaves else head_leaves + current_leaves.append(leaf) + if current_leaves is head_leaves: + if leaf.type in OPENING_BRACKETS: + matching_bracket = leaf + current_leaves = body_leaves + 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) + bracket_split_succeeded_or_raise(head, body, tail) + for result in (head, body, tail): + if result: + yield result + + +def right_hand_split( + line: Line, + line_length: int, + features: Collection[Feature] = (), + omit: Collection[LeafID] = (), +) -> Iterator[Line]: + """Split line into many lines, starting with the last matching bracket pair. + + If the split was by optional parentheses, attempt splitting without them, too. + `omit` is a collection of closing bracket IDs that shouldn't be considered for + this split. + + Note: running this function modifies `bracket_depth` on the leaves of `line`. + """ + tail_leaves: List[Leaf] = [] + body_leaves: List[Leaf] = [] + head_leaves: List[Leaf] = [] + current_leaves = tail_leaves + opening_bracket: Optional[Leaf] = None + closing_bracket: Optional[Leaf] = None + for leaf in reversed(line.leaves): + if current_leaves is body_leaves: + if leaf is opening_bracket: + current_leaves = head_leaves if body_leaves else tail_leaves + current_leaves.append(leaf) + if current_leaves is tail_leaves: + if leaf.type in CLOSING_BRACKETS and id(leaf) not in omit: + opening_bracket = leaf.opening_bracket + closing_bracket = leaf + current_leaves = body_leaves + if not (opening_bracket and closing_bracket and head_leaves): + # If there is no opening or closing_bracket that means the split failed and + # all content is in the tail. Otherwise, if `head_leaves` are empty, it means + # the matching `opening_bracket` wasn't available on `line` anymore. + raise CannotSplit("No brackets found") + + 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) + bracket_split_succeeded_or_raise(head, body, tail) + 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 + # the closing bracket is an optional paren + and closing_bracket.type == token.RPAR + and not 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 we can actually remove the parens + and can_omit_invisible_parens(body, line_length, omit_on_explode=omit) + ): + omit = {id(closing_bracket), *omit} + try: + yield from right_hand_split(line, line_length, features=features, omit=omit) + return + + except CannotSplit: + 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." + ) + + elif head.contains_multiline_strings() or 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" + " contains multiline strings which by definition never fit one" + " line." + ) + + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + for result in (head, body, tail): + if result: + yield result + + +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. + + Do nothing otherwise. + + A left- or right-hand split is based on a pair of brackets. Content before + (and including) the opening bracket is left on one line, content inside the + brackets is put on a separate line, and finally content starting with and + following the closing bracket is put on a separate line. + + Those are called `head`, `body`, and `tail`, respectively. If the split + produced the same line (all content in `head`) or ended up with an empty `body` + and the `tail` is just the closing bracket, then it's considered failed. + """ + tail_len = len(str(tail).strip()) + if not body: + if tail_len == 0: + raise CannotSplit("Splitting brackets produced the same line") + + elif tail_len < 3: + raise CannotSplit( + f"Splitting brackets on an empty body to save {tail_len} characters is" + " not worth it" + ) + + +def bracket_split_build_line( + leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False +) -> Line: + """Return a new line with given `leaves` and respective comments from `original`. + + 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. + """ + result = Line(mode=original.mode, depth=original.depth) + if is_body: + 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 = ( + original.is_def + and opening_bracket.value == "(" + and not any(leaf.type == token.COMMA for leaf in leaves) + ) + + if original.is_import or no_commas: + for i in range(len(leaves) - 1, -1, -1): + if leaves[i].type == STANDALONE_COMMENT: + continue + + if leaves[i].type != token.COMMA: + new_comma = Leaf(token.COMMA, ",") + leaves.insert(i + 1, new_comma) + break + + # Populate the line + for leaf in leaves: + result.append(leaf, preformatted=True) + for comment_after in original.comments_after(leaf): + result.append(comment_after, preformatted=True) + if is_body and should_split_line(result, opening_bracket): + result.should_split_rhs = True + return result + + +def dont_increase_indentation(split_func: Transformer) -> Transformer: + """Normalize prefix of the first leaf in every line returned by `split_func`. + + This is a decorator over relevant split functions. + """ + + @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 + + return split_wrapper + + +@dont_increase_indentation +def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]: + """Split according to delimiters of the highest priority. + + If the appropriate Features are given, the split will add trailing commas + also in function signatures and calls that contain `*` and `**`. + """ + try: + last_leaf = line.leaves[-1] + except IndexError: + raise CannotSplit("Line empty") + + bt = line.bracket_tracker + try: + delimiter_priority = bt.max_delimiter_priority(exclude={id(last_leaf)}) + except ValueError: + raise CannotSplit("No delimiters found") + + if delimiter_priority == DOT_PRIORITY: + if bt.delimiter_count_with_priority(delimiter_priority) == 1: + raise CannotSplit("Splitting a single attribute from its owner looks wrong") + + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) + lowest_depth = sys.maxsize + trailing_comma_safe = True + + def append_to_line(leaf: Leaf) -> Iterator[Line]: + """Append `leaf` to current line or to new line if appending impossible.""" + nonlocal current_line + try: + current_line.append_safe(leaf, preformatted=True) + except ValueError: + yield current_line + + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) + current_line.append(leaf) + + for leaf in line.leaves: + yield from append_to_line(leaf) + + for comment_after in line.comments_after(leaf): + yield from append_to_line(comment_after) + + lowest_depth = min(lowest_depth, leaf.bracket_depth) + if leaf.bracket_depth == lowest_depth: + if is_vararg(leaf, within={syms.typedargslist}): + trailing_comma_safe = ( + trailing_comma_safe and Feature.TRAILING_COMMA_IN_DEF in features + ) + elif is_vararg(leaf, within={syms.arglist, syms.argument}): + trailing_comma_safe = ( + trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features + ) + + leaf_priority = bt.delimiters.get(id(leaf)) + if leaf_priority == delimiter_priority: + yield current_line + + current_line = 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) + yield current_line + + +@dont_increase_indentation +def standalone_comment_split( + line: Line, features: Collection[Feature] = () +) -> Iterator[Line]: + """Split standalone comments from the rest of the line.""" + if not line.contains_standalone_comments(0): + raise CannotSplit("Line does not have any standalone comments") + + current_line = Line( + mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) + + def append_to_line(leaf: Leaf) -> Iterator[Line]: + """Append `leaf` to current line or to new line if appending impossible.""" + nonlocal current_line + try: + current_line.append_safe(leaf, preformatted=True) + except ValueError: + yield current_line + + current_line = Line( + line.mode, depth=line.depth, inside_brackets=line.inside_brackets + ) + current_line.append(leaf) + + for leaf in line.leaves: + yield from append_to_line(leaf) + + for comment_after in line.comments_after(leaf): + yield from append_to_line(comment_after) + + if current_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(node: Node, parens_after: Set[str]) -> None: + """Make existing optional parentheses invisible or create new ones. + + `parens_after` is a set of string leaf values immediately after which parens + should be put. + + 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): + if pc.value in FMT_OFF: + # This `node` has a prefix with `# fmt: off`, don't mess with parens. + return + 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) + + # Add parentheses around long tuple unpacking in assignments. + if ( + index == 0 + and isinstance(child, Node) + and child.type == syms.testlist_star_expr + ): + check_lpar = True + + if check_lpar: + if child.type == syms.atom: + if maybe_make_parens_invisible_in_atom(child, parent=node): + wrap_in_parentheses(node, child, visible=False) + 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 child.type == token.LPAR: + # make parentheses invisible + child.value = "" # type: ignore + node.children[-1].value = "" # type: ignore + elif child.type != token.STAR: + # insert invisible parentheses + node.insert_child(index, Leaf(token.LPAR, "")) + node.append_child(Leaf(token.RPAR, "")) + break + + 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 + + +def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: + """If it's safe, make the parens in the atom `node` invisible, recursively. + 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 ( + 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 + ): + return False + + if is_walrus_assignment(node): + if parent.type in [ + syms.annassign, + syms.expr_stmt, + syms.assert_stmt, + syms.return_stmt, + # these ones aren't useful to end users, but they do please fuzzers + syms.for_stmt, + syms.del_stmt, + ]: + return False + + first = node.children[0] + last = node.children[-1] + if first.type == token.LPAR and last.type == token.RPAR: + middle = node.children[1] + # make parentheses invisible + first.value = "" # type: ignore + last.value = "" # type: ignore + maybe_make_parens_invisible_in_atom(middle, parent=parent) + + if is_atom_with_invisible_parens(middle): + # Strip the invisible parens from `middle` by replacing + # it with the child in-between the invisible parens + middle.replace(middle.children[1]) + + return False + + return True + + +def should_split_line(line: Line, opening_bracket: Leaf) -> bool: + """Should `line` be immediately split with `delimiter_split()` after RHS?""" + + if not (opening_bracket.parent and opening_bracket.value in "[{("): + return False + + # We're essentially checking if the body is delimited by commas and there's more + # than one of them (we're excluding the trailing comma and if the delimiter priority + # is still commas, that means there's more). + exclude = set() + trailing_comma = False + try: + last_leaf = line.leaves[-1] + if last_leaf.type == token.COMMA: + trailing_comma = True + exclude.add(id(last_leaf)) + max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) + except (IndexError, ValueError): + return False + + return max_priority == COMMA_PRIORITY and ( + (line.mode.magic_trailing_comma and trailing_comma) + # always explode imports + or opening_bracket.parent.type in {syms.atom, syms.import_from} + ) + + +def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]: + """Generate sets of closing bracket IDs that should be omitted in a RHS. + + Brackets can be omitted if the entire trailer up to and including + a preceding closing bracket fits in one line. + + Yielded sets are cumulative (contain results of previous yields, too). First + set is empty, unless the line should explode, in which case bracket pairs until + the one that needs to explode are omitted. + """ + + omit: Set[LeafID] = set() + if not line.magic_trailing_comma: + yield omit + + length = 4 * line.depth + 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): + length += leaf_length + if length > line_length: + break + + has_inline_comment = leaf_length > len(leaf.value) + len(leaf.prefix) + if leaf.type == STANDALONE_COMMENT or has_inline_comment: + break + + if opening_bracket: + if leaf is opening_bracket: + opening_bracket = None + elif leaf.type in CLOSING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if ( + prev + and prev.type == token.COMMA + and not is_one_tuple_between( + leaf.opening_bracket, leaf, line.leaves + ) + ): + # Never omit bracket pairs with trailing commas. + # We need to explode on those. + break + + inner_brackets.add(id(leaf)) + elif leaf.type in CLOSING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if prev and prev.type in OPENING_BRACKETS: + # Empty brackets would fail a split so treat them as "inner" + # brackets (e.g. only add them to the `omit` set if another + # pair of brackets was good enough. + inner_brackets.add(id(leaf)) + continue + + if closing_bracket: + omit.add(id(closing_bracket)) + omit.update(inner_brackets) + inner_brackets.clear() + yield omit + + if ( + prev + and prev.type == token.COMMA + and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) + ): + # Never omit bracket pairs with trailing commas. + # We need to explode on those. + break + + if leaf.value: + opening_bracket = leaf.opening_bracket + closing_bracket = leaf + + +def run_transformer( + line: Line, + transform: Transformer, + mode: Mode, + features: Collection[Feature], + *, + line_str: str = "", +) -> List[Line]: + if not line_str: + line_str = line_to_string(line) + result: List[Line] = [] + for transformed_line in transform(line, features): + if str(transformed_line).strip("\n") == line_str: + raise CannotTransform("Line transformer returned an unchanged result") + + result.extend(transform_line(transformed_line, mode=mode, features=features)) + + if not ( + transform.__name__ == "rhs" + and line.bracket_tracker.invisible + and not any(bracket.value for bracket in line.bracket_tracker.invisible) + and not line.contains_multiline_strings() + and not result[0].contains_uncollapsable_type_comments() + and not result[0].contains_unsplittable_type_ignore() + and not is_line_short_enough(result[0], line_length=mode.line_length) + ): + return result + + line_copy = line.clone() + append_leaves(line_copy, line, line.leaves) + features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} + 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 + ): + result = second_opinion + return result diff --git a/src/black/lines.py b/src/black/lines.py new file mode 100644 index 00000000000..63225c0e6d3 --- /dev/null +++ b/src/black/lines.py @@ -0,0 +1,734 @@ +from dataclasses import dataclass, field +import itertools +import sys +from typing import ( + Callable, + Collection, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + TypeVar, + cast, +) + +from blib2to3.pytree import Node, Leaf +from blib2to3.pgen2 import token + +from black.brackets import BracketTracker, DOT_PRIORITY +from black.mode import Mode +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_one_tuple_between + +# types +T = TypeVar("T") +Index = int +LeafID = int + + +@dataclass +class Line: + """Holds leaves and comments. Can be printed with `str(line)`.""" + + mode: Mode + depth: int = 0 + leaves: List[Leaf] = field(default_factory=list) + # keys ordered like `leaves` + comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict) + bracket_tracker: BracketTracker = field(default_factory=BracketTracker) + inside_brackets: bool = False + should_split_rhs: bool = False + magic_trailing_comma: Optional[Leaf] = None + + def append(self, leaf: Leaf, preformatted: bool = False) -> None: + """Add a new `leaf` to the end of the line. + + Unless `preformatted` is True, the `leaf` will receive a new consistent + whitespace prefix and metadata applied by :class:`BracketTracker`. + Trailing commas are maybe removed, unpacked for loop variables are + demoted from being delimiters. + + Inline comments are put aside. + """ + has_value = leaf.type in BRACKETS or bool(leaf.value.strip()) + if not has_value: + return + + if token.COLON == leaf.type and self.is_class_paren_empty: + del self.leaves[-2:] + if self.leaves and not preformatted: + # 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) + ) + if self.inside_brackets or not preformatted: + self.bracket_tracker.mark(leaf) + if self.mode.magic_trailing_comma: + if self.has_magic_trailing_comma(leaf): + self.magic_trailing_comma = leaf + elif self.has_magic_trailing_comma(leaf, ensure_removable=True): + self.remove_trailing_comma() + if not self.append_comment(leaf): + self.leaves.append(leaf) + + def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None: + """Like :func:`append()` but disallow invalid standalone comment structure. + + Raises ValueError when any `leaf` is appended after a standalone comment + or when a standalone comment is not the first leaf on the line. + """ + if self.bracket_tracker.depth == 0: + if self.is_comment: + raise ValueError("cannot append to standalone comments") + + if self.leaves and leaf.type == STANDALONE_COMMENT: + raise ValueError( + "cannot append standalone comments to a populated line" + ) + + self.append(leaf, preformatted=preformatted) + + @property + def is_comment(self) -> bool: + """Is this line a standalone comment?""" + return len(self.leaves) == 1 and self.leaves[0].type == STANDALONE_COMMENT + + @property + def is_decorator(self) -> bool: + """Is this line a decorator?""" + return bool(self) and self.leaves[0].type == token.AT + + @property + def is_import(self) -> bool: + """Is this an import line?""" + return bool(self) and is_import(self.leaves[0]) + + @property + def is_class(self) -> bool: + """Is this line a class definition?""" + return ( + bool(self) + and self.leaves[0].type == token.NAME + and self.leaves[0].value == "class" + ) + + @property + def is_stub_class(self) -> bool: + """Is this line a class definition with a body consisting only of "..."?""" + return self.is_class and self.leaves[-3:] == [ + Leaf(token.DOT, ".") for _ in range(3) + ] + + @property + def is_def(self) -> bool: + """Is this a function definition? (Also returns True for async defs.)""" + try: + first_leaf = self.leaves[0] + except IndexError: + return False + + try: + second_leaf: Optional[Leaf] = self.leaves[1] + except IndexError: + second_leaf = None + return (first_leaf.type == token.NAME and first_leaf.value == "def") or ( + first_leaf.type == token.ASYNC + and second_leaf is not None + and second_leaf.type == token.NAME + and second_leaf.value == "def" + ) + + @property + def is_class_paren_empty(self) -> bool: + """Is this a class with no base classes but using parentheses? + + Those are unnecessary and should be removed. + """ + return ( + bool(self) + and len(self.leaves) == 4 + and self.is_class + and self.leaves[2].type == token.LPAR + and self.leaves[2].value == "(" + and self.leaves[3].type == token.RPAR + and self.leaves[3].value == ")" + ) + + @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(('"""', "'''")) + ) + + def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> 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: + return True + + return False + + def contains_uncollapsable_type_comments(self) -> bool: + ignored_ids = set() + try: + last_leaf = self.leaves[-1] + ignored_ids.add(id(last_leaf)) + if last_leaf.type == token.COMMA or ( + last_leaf.type == token.RPAR and not last_leaf.value + ): + # When trailing commas or optional parens are inserted by Black for + # consistency, comments after the previous last element are not moved + # (they don't have to, rendering will still be correct). So we ignore + # trailing commas and invisible. + last_leaf = self.leaves[-2] + ignored_ids.add(id(last_leaf)) + except IndexError: + return False + + # A type comment is uncollapsable if it is attached to a leaf + # that isn't at the end of the line (since that could cause it + # to get associated to a different argument) or if there are + # comments before it (since that could cause it to get hidden + # behind a comment. + comment_seen = False + for leaf_id, comments in self.comments.items(): + for comment in comments: + if is_type_comment(comment): + if comment_seen or ( + not is_type_comment(comment, " ignore") + and leaf_id not in ignored_ids + ): + return True + + comment_seen = True + + return False + + def contains_unsplittable_type_ignore(self) -> bool: + if not self.leaves: + return False + + # If a 'type: ignore' is attached to the end of a line, we + # can't split the line, because we can't know which of the + # subexpressions the ignore was meant to apply to. + # + # We only want this to apply to actual physical lines from the + # original source, though: we don't want the presence of a + # 'type: ignore' at the end of a multiline expression to + # justify pushing it all onto one line. Thus we + # (unfortunately) need to check the actual source lines and + # only report an unsplittable 'type: ignore' if this line was + # one line in the original code. + + # Grab the first and last line numbers, skipping generated leaves + first_line = next((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0) + last_line = next( + (leaf.lineno for leaf in reversed(self.leaves) if leaf.lineno != 0), 0 + ) + + if first_line == last_line: + # We look at the last two leaves since a comma or an + # invisible paren could have been added at the end of the + # line. + for node in self.leaves[-2:]: + for comment in self.comments.get(id(node), []): + if is_type_comment(comment, " ignore"): + return True + + return False + + def contains_multiline_strings(self) -> bool: + return any(is_multiline_string(leaf) for leaf in self.leaves) + + def has_magic_trailing_comma( + self, closing: Leaf, ensure_removable: bool = False + ) -> bool: + """Return True if we have a magic trailing comma, that is when: + - there's a trailing comma here + - it's not a one-tuple + Additionally, if ensure_removable: + - it's not from square bracket indexing + """ + if not ( + closing.type in CLOSING_BRACKETS + and self.leaves + and self.leaves[-1].type == token.COMMA + ): + return False + + if closing.type == token.RBRACE: + return True + + if closing.type == token.RSQB: + if not ensure_removable: + return True + comma = self.leaves[-1] + return bool(comma.parent and comma.parent.type == syms.listmaker) + + if self.is_import: + return True + + if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves): + return True + + return False + + def append_comment(self, comment: Leaf) -> bool: + """Add an inline or standalone comment to the line.""" + if ( + comment.type == STANDALONE_COMMENT + and self.bracket_tracker.any_open_brackets() + ): + comment.prefix = "" + return False + + if comment.type != token.COMMENT: + return False + + if not self.leaves: + comment.type = STANDALONE_COMMENT + comment.prefix = "" + return False + + last_leaf = self.leaves[-1] + if ( + last_leaf.type == token.RPAR + and not last_leaf.value + and last_leaf.parent + and len(list(last_leaf.parent.leaves())) <= 3 + and not is_type_comment(comment) + ): + # Comments on an optional parens wrapping a single leaf should belong to + # the wrapped node except if it's a type comment. Pinning the comment like + # this avoids unstable formatting caused by comment migration. + if len(self.leaves) < 2: + comment.type = STANDALONE_COMMENT + comment.prefix = "" + return False + + last_leaf = self.leaves[-2] + self.comments.setdefault(id(last_leaf), []).append(comment) + return True + + def comments_after(self, leaf: Leaf) -> List[Leaf]: + """Generate comments that should appear directly after `leaf`.""" + return self.comments.get(id(leaf), []) + + def remove_trailing_comma(self) -> None: + """Remove the trailing comma and moves the comments attached to it.""" + trailing_comma = self.leaves.pop() + trailing_comma_comments = self.comments.pop(id(trailing_comma), []) + self.comments.setdefault(id(self.leaves[-1]), []).extend( + trailing_comma_comments + ) + + def is_complex_subscript(self, leaf: Leaf) -> bool: + """Return True iff `leaf` is part of a slice with non-trivial exprs.""" + open_lsqb = self.bracket_tracker.get_open_lsqb() + if open_lsqb is None: + return False + + subscript_start = open_lsqb.next_sibling + + if isinstance(subscript_start, Node): + if subscript_start.type == syms.listmaker: + return False + + if subscript_start.type == syms.subscriptlist: + subscript_start = child_towards(subscript_start, leaf) + return subscript_start is not None and any( + n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() + ) + + def enumerate_with_length( + self, reversed: bool = False + ) -> Iterator[Tuple[Index, Leaf, int]]: + """Return an enumeration of leaves with their length. + + Stops prematurely on multiline strings and standalone comments. + """ + op = cast( + Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]], + enumerate_reversed if reversed else enumerate, + ) + for index, leaf in op(self.leaves): + length = len(leaf.prefix) + len(leaf.value) + if "\n" in leaf.value: + return # Multiline strings, we can't continue. + + for comment in self.comments_after(leaf): + length += len(comment.value) + + yield index, leaf, length + + def clone(self) -> "Line": + return Line( + mode=self.mode, + depth=self.depth, + inside_brackets=self.inside_brackets, + should_split_rhs=self.should_split_rhs, + magic_trailing_comma=self.magic_trailing_comma, + ) + + def __str__(self) -> str: + """Render the line.""" + if not self: + return "\n" + + indent = " " * self.depth + leaves = iter(self.leaves) + first = next(leaves) + res = f"{first.prefix}{indent}{first.value}" + for leaf in leaves: + res += str(leaf) + for comment in itertools.chain.from_iterable(self.comments.values()): + res += str(comment) + + return res + "\n" + + def __bool__(self) -> bool: + """Return True if the line has leaves or comments.""" + return bool(self.leaves or self.comments) + + +@dataclass +class EmptyLineTracker: + """Provides a stateful method that returns the number of potential extra + empty lines needed before and after the currently processed line. + + Note: this tracker works on lines that haven't been split yet. It assumes + the prefix of the first leaf consists of optional newlines. Those newlines + are consumed by `maybe_empty_lines()` and included in the computation. + """ + + is_pyi: bool = False + previous_line: Optional[Line] = None + previous_after: int = 0 + previous_defs: List[int] = field(default_factory=list) + + def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: + """Return the number of extra empty lines before and after the `current_line`. + + 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) + 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 + ) + self.previous_after = after + self.previous_line = current_line + return before, after + + 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 + if current_line.leaves: + # Consume the first leaf's extra newlines. + first_leaf = current_line.leaves[0] + before = first_leaf.prefix.count("\n") + before = min(before, max_allowed) + first_leaf.prefix = "" + else: + 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 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) + + if ( + self.previous_line + and self.previous_line.is_import + and not current_line.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_triple_quoted_string + ): + return before, 1 + + return before, 0 + + 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) + if self.previous_line is None: + # Don't insert empty lines before the first line in the file. + return 0, 0 + + if self.previous_line.is_decorator: + if self.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 ( + self.previous_line.is_class or self.previous_line.is_def + ): + return 0, 0 + + if ( + self.previous_line.is_comment + and self.previous_line.depth == current_line.depth + and before == 0 + ): + return 0, 0 + + if self.is_pyi: + if self.previous_line.depth > current_line.depth: + newlines = 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: + # No blank line between classes with an empty body + newlines = 0 + else: + newlines = 1 + 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 + else: + newlines = 0 + else: + newlines = 2 + if current_line.depth and newlines: + newlines -= 1 + return newlines, 0 + + +def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]: + """Like `reversed(enumerate(sequence))` if that were possible.""" + index = len(sequence) - 1 + for element in reversed(sequence): + yield (index, element) + index -= 1 + + +def append_leaves( + new_line: Line, old_line: Line, leaves: List[Leaf], preformatted: bool = False +) -> None: + """ + Append leaves (taken from @old_line) to @new_line, making sure to fix the + underlying Node structure where appropriate. + + All of the leaves in @leaves are duplicated. The duplicates are then + appended to @new_line and used to replace their originals in the underlying + Node structure. Any comments attached to the old leaves are reattached to + the new leaves. + + Pre-conditions: + set(@leaves) is a subset of set(@old_line.leaves). + """ + for old_leaf in leaves: + new_leaf = Leaf(old_leaf.type, old_leaf.value) + replace_child(old_leaf, new_leaf) + new_line.append(new_leaf, preformatted=preformatted) + + for comment_leaf in old_line.comments_after(old_leaf): + 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`. + + 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() + ) + + +def can_be_split(line: Line) -> bool: + """Return False if the line cannot be split *for sure*. + + This is not an exhaustive search but a cheap heuristic that we can use to + avoid some unfortunate formattings (mostly around wrapping unsplittable code + in unnecessary parentheses). + """ + leaves = line.leaves + if len(leaves) < 2: + return False + + if leaves[0].type == token.STRING and leaves[1].type == token.DOT: + call_count = 0 + dot_count = 0 + next = leaves[-1] + for leaf in leaves[-2::-1]: + if leaf.type in OPENING_BRACKETS: + if next.type not in CLOSING_BRACKETS: + return False + + call_count += 1 + elif leaf.type == token.DOT: + dot_count += 1 + elif leaf.type == token.NAME: + if not (next.type == token.DOT or next.type in OPENING_BRACKETS): + return False + + elif leaf.type not in CLOSING_BRACKETS: + return False + + if dot_count > 1 and call_count > 1: + return False + + return True + + +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? + + 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. + """ + 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: + # With more than one delimiter of a kind the optional parentheses read better. + return False + + if max_priority == DOT_PRIORITY: + # A single stranded method call doesn't require optional parentheses. + return True + + assert len(line.leaves) >= 2, "Stranded delimiter" + + # With a single delimiter, omit if the expression starts or ends with + # a bracket. + first = line.leaves[0] + second = line.leaves[1] + if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS: + if _can_omit_opening_paren(line, first=first, line_length=line_length): + return True + + # Note: we are not returning False here because a line might have *both* + # a leading opening bracket and a trailing closing bracket. If the + # opening bracket doesn't match our rule, maybe the closing will. + + 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 + or last.type == token.RBRACE + or ( + # don't use indexing for omitting optional parentheses; + # it looks weird + last.type == token.RSQB + and last.parent + and last.parent.type != syms.trailer + ) + ): + if penultimate.type in OPENING_BRACKETS: + # Empty brackets don't help. + return False + + if is_multiline_string(first): + # Additional wrapping of a multiline string in this situation is + # 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 + + return False + + +def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + remainder = False + length = 4 * line.depth + _index = -1 + for _index, leaf, leaf_length in line.enumerate_with_length(): + if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: + remainder = True + if remainder: + length += leaf_length + if length > line_length: + break + + if leaf.type in OPENING_BRACKETS: + # There are brackets we can further split on. + remainder = False + + else: + # checked the entire string and line length wasn't exceeded + if len(line.leaves) == _index + 1: + return True + + return False + + +def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + length = 4 * line.depth + seen_other_brackets = False + for _index, leaf, leaf_length in line.enumerate_with_length(): + length += leaf_length + if leaf is last.opening_bracket: + if seen_other_brackets or length <= line_length: + return True + + elif leaf.type in OPENING_BRACKETS: + # There are brackets we can further split on. + seen_other_brackets = True + + return False + + +def line_to_string(line: Line) -> str: + """Returns the string representation of @line. + + WARNING: This is known to be computationally expensive. + """ + return str(line).strip("\n") diff --git a/src/black/mode.py b/src/black/mode.py new file mode 100644 index 00000000000..e2ce322da5c --- /dev/null +++ b/src/black/mode.py @@ -0,0 +1,123 @@ +"""Data structures configuring Black behavior. + +Mostly around Python language feature support per version and Black configuration +chosen by the user. +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Set + +from black.const import DEFAULT_LINE_LENGTH + + +class TargetVersion(Enum): + PY27 = 2 + PY33 = 3 + PY34 = 4 + PY35 = 5 + PY36 = 6 + PY37 = 7 + PY38 = 8 + PY39 = 9 + + 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 + TRAILING_COMMA_IN_DEF = 5 + # The following two feature-flags are mutually exclusive, and exactly one should be + # set for every version of python. + ASYNC_IDENTIFIERS = 6 + ASYNC_KEYWORDS = 7 + ASSIGNMENT_EXPRESSIONS = 8 + POS_ONLY_ARGUMENTS = 9 + RELAXED_DECORATORS = 10 + FORCE_OPTIONAL_PARENTHESES = 50 + + +VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { + TargetVersion.PY27: {Feature.ASYNC_IDENTIFIERS}, + 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.PY36: { + Feature.UNICODE_LITERALS, + Feature.F_STRINGS, + Feature.NUMERIC_UNDERSCORES, + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.ASYNC_IDENTIFIERS, + }, + TargetVersion.PY37: { + Feature.UNICODE_LITERALS, + Feature.F_STRINGS, + Feature.NUMERIC_UNDERSCORES, + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.ASYNC_KEYWORDS, + }, + TargetVersion.PY38: { + 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.POS_ONLY_ARGUMENTS, + }, + TargetVersion.PY39: { + 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, + }, +} + + +def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> bool: + return all(feature in VERSION_TO_FEATURES[version] for version in target_versions) + + +@dataclass +class Mode: + target_versions: Set[TargetVersion] = field(default_factory=set) + line_length: int = DEFAULT_LINE_LENGTH + string_normalization: bool = True + is_pyi: bool = False + magic_trailing_comma: bool = True + experimental_string_processing: bool = False + + 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) + ) + else: + version_str = "-" + parts = [ + version_str, + str(self.line_length), + str(int(self.string_normalization)), + str(int(self.is_pyi)), + str(int(self.magic_trailing_comma)), + str(int(self.experimental_string_processing)), + ] + return ".".join(parts) diff --git a/src/black/nodes.py b/src/black/nodes.py new file mode 100644 index 00000000000..e0db9a42426 --- /dev/null +++ b/src/black/nodes.py @@ -0,0 +1,843 @@ +""" +blib2to3 Node/Leaf transformation-related utility functions. +""" + +import sys +from typing import ( + Collection, + Generic, + Iterator, + List, + Optional, + Set, + Tuple, + TypeVar, + 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, type_repr +from blib2to3 import pygram +from blib2to3.pgen2 import token + +from black.cache import CACHE_DIR +from black.strings import has_triple_quotes + + +pygram.initialize(CACHE_DIR) +syms = pygram.python_symbols + + +# types +T = TypeVar("T") +LN = Union[Leaf, Node] +LeafID = int +NodeType = int + + +WHITESPACE: Final = {token.DEDENT, token.INDENT, token.NEWLINE} +STATEMENT: Final = { + syms.if_stmt, + syms.while_stmt, + syms.for_stmt, + syms.try_stmt, + syms.except_clause, + syms.with_stmt, + syms.funcdef, + syms.classdef, +} +STANDALONE_COMMENT: Final = 153 +token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT" +LOGIC_OPERATORS: Final = {"and", "or"} +COMPARATORS: Final = { + token.LESS, + token.GREATER, + token.EQEQUAL, + token.NOTEQUAL, + token.LESSEQUAL, + token.GREATEREQUAL, +} +MATH_OPERATORS: Final = { + token.VBAR, + token.CIRCUMFLEX, + token.AMPER, + token.LEFTSHIFT, + token.RIGHTSHIFT, + token.PLUS, + token.MINUS, + token.STAR, + token.SLASH, + token.DOUBLESLASH, + token.PERCENT, + token.AT, + token.TILDE, + token.DOUBLESTAR, +} +STARS: Final = {token.STAR, token.DOUBLESTAR} +VARARGS_SPECIALS: Final = STARS | {token.SLASH} +VARARGS_PARENTS: Final = { + syms.arglist, + syms.argument, # double star in arglist + syms.trailer, # single argument to call + syms.typedargslist, + syms.varargslist, # lambdas +} +UNPACKING_PARENTS: Final = { + syms.atom, # single element of a list or set literal + syms.dictsetmaker, + syms.listmaker, + syms.testlist_gexp, + syms.testlist_star_expr, +} +TEST_DESCENDANTS: Final = { + syms.test, + syms.lambdef, + syms.or_test, + syms.and_test, + syms.not_test, + syms.comparison, + syms.star_expr, + syms.expr, + syms.xor_expr, + syms.and_expr, + syms.shift_expr, + syms.arith_expr, + syms.trailer, + syms.term, + syms.power, +} +ASSIGNMENTS: Final = { + "=", + "+=", + "-=", + "*=", + "@=", + "/=", + "%=", + "&=", + "|=", + "^=", + "<<=", + ">>=", + "**=", + "//=", +} + +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} + + +class Visitor(Generic[T]): + """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" + + def visit(self, node: LN) -> Iterator[T]: + """Main method to visit `node` and its children. + + It tries to find a `visit_*()` method for the given `node.type`, like + `visit_simple_stmt` for Node objects or `visit_INDENT` for Leaf objects. + If no dedicated `visit_*()` method is found, chooses `visit_default()` + instead. + + Then yields objects of type `T` from the selected visitor. + """ + if node.type < 256: + name = token.tok_name[node.type] + else: + name = str(type_repr(node.type)) + # We explicitly branch on whether a visitor exists (instead of + # using self.visit_default as the default arg to getattr) in order + # to save needing to create a bound method object and so mypyc can + # generate a native call to visit_default. + visitf = getattr(self, f"visit_{name}", None) + if visitf: + yield from visitf(node) + else: + yield from self.visit_default(node) + + def visit_default(self, node: LN) -> Iterator[T]: + """Default `visit_*()` implementation. Recurses to children of `node`.""" + if isinstance(node, Node): + for child in node.children: + yield from self.visit(child) + + +def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 + """Return whitespace prefix if needed for the given `leaf`. + + `complex_subscript` signals whether the given leaf is part of a subscription + which has non-trivial arguments, like arithmetic expressions or function calls. + """ + NO = "" + SPACE = " " + DOUBLESPACE = " " + t = leaf.type + p = leaf.parent + v = leaf.value + if t in ALWAYS_NO_SPACE: + return NO + + if t == token.COMMENT: + return DOUBLESPACE + + assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" + if t == token.COLON and p.type not in { + syms.subscript, + syms.subscriptlist, + syms.sliceop, + }: + return NO + + prev = leaf.prev_sibling + if not prev: + prevp = preceding_leaf(p) + if not prevp or prevp.type in OPENING_BRACKETS: + return NO + + if t == token.COLON: + if prevp.type == token.COLON: + return NO + + elif prevp.type != token.COMMA and not complex_subscript: + return NO + + return SPACE + + if prevp.type == token.EQUAL: + if prevp.parent: + if prevp.parent.type in { + syms.arglist, + syms.argument, + syms.parameters, + syms.varargslist, + }: + return NO + + elif prevp.parent.type == syms.typedargslist: + # A bit hacky: if the equal sign has whitespace, it means we + # previously found it's a typed argument. So, we're using + # that, too. + return prevp.prefix + + elif prevp.type in VARARGS_SPECIALS: + if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS): + return NO + + elif prevp.type == token.COLON: + if prevp.parent and prevp.parent.type in {syms.subscript, syms.sliceop}: + return SPACE if complex_subscript else NO + + elif ( + prevp.parent + and prevp.parent.type == syms.factor + and prevp.type in MATH_OPERATORS + ): + return NO + + elif ( + prevp.type == token.RIGHTSHIFT + 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 + ): + # 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 + + elif prev.type in OPENING_BRACKETS: + return NO + + if p.type in {syms.parameters, syms.arglist}: + # untyped function signatures or calls + if not prev or prev.type != token.COMMA: + return NO + + elif p.type == syms.varargslist: + # lambdas + if prev and prev.type != token.COMMA: + return NO + + elif p.type == syms.typedargslist: + # typed function signatures + if not prev: + return NO + + if t == token.EQUAL: + if prev.type != syms.tname: + return NO + + elif prev.type == token.EQUAL: + # A bit hacky: if the equal sign has whitespace, it means we + # previously found it's a typed argument. So, we're using that, too. + return prev.prefix + + elif prev.type != token.COMMA: + return NO + + elif p.type == syms.tname: + # type names + if not prev: + prevp = preceding_leaf(p) + if not prevp or prevp.type != token.COMMA: + return NO + + elif p.type == syms.trailer: + # attributes and calls + if t == token.LPAR or t == token.RPAR: + 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: + return NO + + elif prev.type != token.COMMA: + return NO + + elif p.type == syms.argument: + # single argument + if t == token.EQUAL: + return NO + + if not prev: + prevp = preceding_leaf(p) + if not prevp or prevp.type == token.LPAR: + return NO + + elif prev.type in {token.EQUAL} | VARARGS_SPECIALS: + return NO + + elif p.type == syms.decorator: + # decorators + return NO + + elif p.type == syms.dotted_name: + if prev: + return NO + + prevp = preceding_leaf(p) + if not prevp or prevp.type == token.AT or prevp.type == token.DOT: + return NO + + elif p.type == syms.classdef: + if t == token.LPAR: + return NO + + if prev and prev.type == token.LPAR: + return NO + + elif p.type in {syms.subscript, syms.sliceop}: + # indexing + if not prev: + assert p.parent is not None, "subscripts are always parented" + if p.parent.type == syms.subscriptlist: + return SPACE + + return NO + + elif not complex_subscript: + return NO + + elif p.type == syms.atom: + if prev and t == token.DOT: + # dots, but not the first one. + return NO + + elif p.type == syms.dictsetmaker: + # dict unpacking + if prev and prev.type == token.DOUBLESTAR: + return NO + + elif p.type in {syms.factor, syms.star_expr}: + # unary ops + if not prev: + prevp = preceding_leaf(p) + if not prevp or prevp.type in OPENING_BRACKETS: + return NO + + prevp_parent = prevp.parent + assert prevp_parent is not None + if prevp.type == token.COLON and prevp_parent.type in { + syms.subscript, + syms.sliceop, + }: + return NO + + elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument: + return NO + + elif t in {token.NAME, token.NUMBER, token.STRING}: + return NO + + elif p.type == syms.import_from: + if t == token.DOT: + if prev and prev.type == token.DOT: + return NO + + elif t == token.NAME: + if v == "import": + return SPACE + + if prev and prev.type == token.DOT: + return NO + + elif p.type == syms.sliceop: + return NO + + return SPACE + + +def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: + """Return the first leaf that precedes `node`, if any.""" + while node: + res = node.prev_sibling + if res: + if isinstance(res, Leaf): + return res + + try: + return list(res.leaves())[-1] + + except IndexError: + return None + + node = node.parent + return None + + +def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool: + """Return if the `node` and its previous siblings match types against the provided + list of tokens; the provided `node`has its type matched against the last element in + the list. `None` can be used as the first element to declare that the start of the + list is anchored at the start of its parent's children.""" + if not tokens: + return True + if tokens[-1] is None: + return node is None + if not node: + return False + if node.type != tokens[-1]: + return False + 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 = None + last = 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: + @node.parent.type, if @node is not None and has a parent. + OR + None, otherwise. + """ + if node is None or node.parent is None: + return None + + return node.parent.type + + +def child_towards(ancestor: Node, descendant: LN) -> Optional[LN]: + """Return the child of `ancestor` that contains `descendant`.""" + node: Optional[LN] = descendant + while node and node.parent != ancestor: + node = node.parent + return node + + +def replace_child(old_child: LN, new_child: LN) -> None: + """ + Side Effects: + * If @old_child.parent is set, replace @old_child with @new_child in + @old_child's underlying Node structure. + OR + * Otherwise, this function does nothing. + """ + parent = old_child.parent + if not parent: + return + + child_idx = old_child.remove() + if child_idx is not None: + parent.insert_child(child_idx, new_child) + + +def container_of(leaf: Leaf) -> LN: + """Return `leaf` or one of its ancestors that is the topmost container of it. + + By "container" we mean a node where `leaf` is the very first child. + """ + same_prefix = leaf.prefix + container: LN = leaf + while container: + parent = container.parent + if parent is None: + break + + if parent.children[0].prefix != same_prefix: + break + + if parent.type == syms.file_input: + break + + if parent.prev_sibling is not None and parent.prev_sibling.type in BRACKETS: + break + + container = parent + 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_child_is_arith(node: Node) -> bool: + """Whether first child is an arithmetic or a binary arithmetic expression""" + expr_types = { + 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: + if prev_siblings_are( + leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] + ): + return True + + # Multiline docstring on the same line as the `def`. + if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]): + # `syms.parameters` is only used in funcdefs and async_funcdefs in the Python + # grammar. We're safe to return True without further checks. + return True + + return False + + +def is_empty_tuple(node: LN) -> bool: + """Return True if `node` holds an empty tuple.""" + return ( + node.type == syms.atom + and len(node.children) == 2 + and node.children[0].type == token.LPAR + and node.children[1].type == token.RPAR + ) + + +def is_one_tuple(node: LN) -> bool: + """Return True if `node` holds a tuple with one element, with or without parens.""" + if node.type == syms.atom: + gexp = unwrap_singleton_parenthesis(node) + if gexp is None or gexp.type != syms.testlist_gexp: + return False + + return len(gexp.children) == 2 and gexp.children[1].type == token.COMMA + + return ( + node.type in IMPLICIT_TUPLE + and len(node.children) == 2 + and node.children[1].type == token.COMMA + ) + + +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: + return False + + depth = closing.bracket_depth + 1 + for _opening_index, leaf in enumerate(leaves): + if leaf is opening: + break + + else: + raise LookupError("Opening paren not found in `leaves`") + + commas = 0 + _opening_index += 1 + for leaf in leaves[_opening_index:]: + if leaf is closing: + break + + bracket_depth = leaf.bracket_depth + if bracket_depth == depth and leaf.type == token.COMMA: + commas += 1 + if leaf.parent and leaf.parent.type in { + syms.arglist, + syms.typedargslist, + }: + commas += 1 + break + + return commas < 2 + + +def is_walrus_assignment(node: LN) -> bool: + """Return True iff `node` is of the shape ( test := test )""" + inner = unwrap_singleton_parenthesis(node) + return inner is not None and inner.type == syms.namedexpr_test + + +def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: + """Return True iff `node` is a trailer valid in a simple decorator""" + return node.type == syms.trailer and ( + ( + len(node.children) == 2 + and node.children[0].type == token.DOT + and node.children[1].type == token.NAME + ) + # last trailer can be an argument-less parentheses pair + or ( + last + and len(node.children) == 2 + and node.children[0].type == token.LPAR + and node.children[1].type == token.RPAR + ) + # last trailer can be arguments + or ( + last + and len(node.children) == 3 + and node.children[0].type == token.LPAR + # and node.children[1].type == syms.argument + and node.children[2].type == token.RPAR + ) + ) + + +def is_simple_decorator_expression(node: LN) -> bool: + """Return True iff `node` could be a 'dotted name' decorator + + This function takes the node of the 'namedexpr_test' of the new decorator + grammar and test if it would be valid under the old decorator grammar. + + The old grammar was: decorator: @ dotted_name [arguments] NEWLINE + The new grammar is : decorator: @ namedexpr_test NEWLINE + """ + if node.type == token.NAME: + return True + if node.type == syms.power: + if node.children: + return ( + node.children[0].type == token.NAME + and all(map(is_simple_decorator_trailer, node.children[1:-1])) + and ( + len(node.children) < 2 + or is_simple_decorator_trailer(node.children[-1], last=True) + ) + ) + return False + + +def is_yield(node: LN) -> bool: + """Return True if `node` holds a `yield` or `yield from` expression.""" + if node.type == syms.yield_expr: + return True + + if node.type == token.NAME and node.value == "yield": # type: ignore + return True + + if node.type != syms.atom: + return False + + if len(node.children) != 3: + return False + + lpar, expr, rpar = node.children + if lpar.type == token.LPAR and rpar.type == token.RPAR: + return is_yield(expr) + + return False + + +def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool: + """Return True if `leaf` is a star or double star in a vararg or kwarg. + + If `within` includes VARARGS_PARENTS, this applies to function signatures. + If `within` includes UNPACKING_PARENTS, it applies to right hand-side + extended iterable unpacking (PEP 3132) and additional unpacking + generalizations (PEP 448). + """ + if leaf.type not in VARARGS_SPECIALS or not leaf.parent: + return False + + p = leaf.parent + if p.type == syms.star_expr: + # Star expressions are also used as assignment targets in extended + # iterable unpacking (PEP 3132). See what its parent is instead. + if not p.parent: + return False + + p = p.parent + + return p.type in within + + +def is_multiline_string(leaf: Leaf) -> bool: + """Return True if `leaf` is a multiline string that actually spans many lines.""" + return has_triple_quotes(leaf.value) and "\n" in leaf.value + + +def is_stub_suite(node: Node) -> bool: + """Return True if `node` is a suite with a stub body.""" + if ( + len(node.children) != 4 + or node.children[0].type != token.NEWLINE + or node.children[1].type != token.INDENT + or node.children[3].type != token.DEDENT + ): + return False + + return is_stub_body(node.children[2]) + + +def is_stub_body(node: LN) -> bool: + """Return True if `node` is a simple statement containing an ellipsis.""" + if not isinstance(node, Node) or node.type != syms.simple_stmt: + return False + + if len(node.children) != 2: + return False + + child = node.children[0] + return ( + child.type == syms.atom + and len(child.children) == 3 + and all(leaf == Leaf(token.DOT, ".") for leaf in child.children) + ) + + +def is_atom_with_invisible_parens(node: LN) -> bool: + """Given a `LN`, determines whether it's an atom `node` with invisible + parens. Useful in dedupe-ing and normalizing parens. + """ + if isinstance(node, Leaf) or node.type != syms.atom: + return False + + first, last = node.children[0], node.children[-1] + return ( + isinstance(first, Leaf) + and first.type == token.LPAR + and first.value == "" + and isinstance(last, Leaf) + and last.type == token.RPAR + and last.value == "" + ) + + +def is_empty_par(leaf: Leaf) -> bool: + return is_empty_lpar(leaf) or is_empty_rpar(leaf) + + +def is_empty_lpar(leaf: Leaf) -> bool: + return leaf.type == token.LPAR and leaf.value == "" + + +def is_empty_rpar(leaf: Leaf) -> bool: + return leaf.type == token.RPAR and leaf.value == "" + + +def is_import(leaf: Leaf) -> bool: + """Return True if the given leaf starts an import statement.""" + p = leaf.parent + t = leaf.type + v = leaf.value + return bool( + t == token.NAME + and ( + (v == "import" and p and p.type == syms.import_name) + or (v == "from" and p and p.type == syms.import_from) + ) + ) + + +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.""" + t = leaf.type + v = leaf.value + return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix) + + +def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: + """Wrap `child` in parentheses. + + This replaces `child` with an atom holding the parentheses and the old + child. That requires moving the prefix. + + If `visible` is False, the leaves will be valueless (and thus invisible). + """ + lpar = Leaf(token.LPAR, "(" if visible else "") + rpar = Leaf(token.RPAR, ")" if visible else "") + prefix = child.prefix + child.prefix = "" + index = child.remove() or 0 + new_child = Node(syms.atom, [lpar, child, rpar]) + new_child.prefix = prefix + parent.insert_child(index, new_child) + + +def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: + """Returns `wrapped` if `node` is of the shape ( wrapped ). + + Parenthesis can be optional. Returns None otherwise""" + if len(node.children) != 3: + return None + + lpar, wrapped, rpar = node.children + if not (lpar.type == token.LPAR and rpar.type == token.RPAR): + return None + + return wrapped + + +def ensure_visible(leaf: Leaf) -> None: + """Make sure parentheses are visible. + + They could be invisible as part of some statements (see + :func:`normalize_invisible_parens` and :func:`visit_import_from`). + """ + if leaf.type == token.LPAR: + leaf.value = "(" + elif leaf.type == token.RPAR: + leaf.value = ")" diff --git a/src/black/numerics.py b/src/black/numerics.py new file mode 100644 index 00000000000..cb1c83e7b78 --- /dev/null +++ b/src/black/numerics.py @@ -0,0 +1,65 @@ +""" +Formatting numeric literals. +""" +from blib2to3.pytree import Leaf + + +def format_hex(text: str) -> str: + """ + Formats a hexadecimal string like "0x12B3" + """ + before, after = text[:2], text[2:] + return f"{before}{after.upper()}" + + +def format_scientific_notation(text: str) -> str: + """Formats a numeric string utilizing scentific notation""" + before, after = text.split("e") + sign = "" + if after.startswith("-"): + after = after[1:] + sign = "-" + elif after.startswith("+"): + after = after[1:] + before = format_float_or_int_string(before) + 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`""" + 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}" + + +def format_float_or_int_string(text: str) -> str: + """Formats a float string like "1.0".""" + if "." not in text: + return text + + before, after = text.split(".") + return f"{before or 0}.{after or 0}" + + +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). + """ + text = leaf.value.lower() + if text.startswith(("0o", "0b")): + # Leave octal and binary literals alone. + pass + elif text.startswith("0x"): + 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) + else: + text = format_float_or_int_string(text) + leaf.value = text diff --git a/src/black/output.py b/src/black/output.py new file mode 100644 index 00000000000..6831524090d --- /dev/null +++ b/src/black/output.py @@ -0,0 +1,83 @@ +"""Nice output for Black. + +The double calls are for patching purposes in tests. +""" + +from typing import Any, Optional +from mypy_extensions import mypyc_attr +import tempfile + +from click import echo, style + + +def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: + if message is not None: + if "bold" not in styles: + styles["bold"] = True + message = style(message, **styles) + echo(message, nl=nl, err=True) + + +def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: + if message is not None: + if "fg" not in styles: + styles["fg"] = "red" + message = style(message, **styles) + echo(message, nl=nl, err=True) + + +def out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: + _out(message, nl=nl, **styles) + + +def err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: + _err(message, nl=nl, **styles) + + +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)] + diff_lines = [] + for line in difflib.unified_diff( + a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 + ): + # Work around https://bugs.python.org/issue2142 + # See https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html + if line[-1] == "\n": + diff_lines.append(line) + else: + diff_lines.append(line + "\n") + diff_lines.append("\\ No newline at end of file\n") + return "".join(diff_lines) + + +def color_diff(contents: str) -> str: + """Inject the ANSI color codes to the diff.""" + 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 + elif line.startswith("@@"): + line = "\033[36m" + line + "\033[0m" # cyan, reset + elif line.startswith("+"): + line = "\033[32m" + line + "\033[0m" # green, reset + elif line.startswith("-"): + line = "\033[31m" + line + "\033[0m" # red, reset + lines[i] = line + return "\n".join(lines) + + +@mypyc_attr(patchable=True) +def dump_to_file(*output: str, ensure_final_newline: bool = True) -> str: + """Dump `output` to a temporary file. Return path to the file.""" + with tempfile.NamedTemporaryFile( + mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8" + ) as f: + for lines in output: + f.write(lines) + if ensure_final_newline and lines and lines[-1] != "\n": + f.write("\n") + return f.name diff --git a/src/black/parsing.py b/src/black/parsing.py new file mode 100644 index 00000000000..8e9feea9120 --- /dev/null +++ b/src/black/parsing.py @@ -0,0 +1,215 @@ +""" +Parse Python code and perform AST validation. +""" +import ast +import sys +from typing import Iterable, Iterator, List, Set, Union + +# lib2to3 fork +from blib2to3.pytree import Node, Leaf +from blib2to3 import pygram, pytree +from blib2to3.pgen2 import driver +from blib2to3.pgen2.grammar import Grammar +from blib2to3.pgen2.parse import ParseError + +from black.mode import TargetVersion, Feature, supports_feature +from black.nodes import syms + +try: + from typed_ast import ast3, ast27 +except ImportError: + if sys.version_info < (3, 8): + 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 = ast27 = ast + + +class InvalidInput(ValueError): + """Raised when input source code fails all parse attempts.""" + + +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, + ] + + 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 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+ + 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) + # 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" + + for grammar in get_grammars(set(target_versions)): + drv = driver.Driver(grammar, pytree.convert) + try: + result = drv.parse_string(src_txt, True) + break + + except ParseError as pe: + lineno, column = pe.context[1] + lines = src_txt.splitlines() + try: + faulty_line = lines[lineno - 1] + except IndexError: + faulty_line = "" + exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}") + else: + raise exc from None + + if isinstance(result, Leaf): + result = Node(syms.file_input, [result]) + return result + + +def lib2to3_unparse(node: Node) -> str: + """Given a lib2to3 node, return its string representation.""" + code = str(node) + return code + + +def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: + filename = "" + if sys.version_info >= (3, 8): + # TODO: support Python 4+ ;) + for minor_version in range(sys.version_info[1], 4, -1): + try: + return ast.parse(src, filename, feature_version=(3, minor_version)) + except SyntaxError: + continue + else: + for feature_version in (7, 6): + try: + return ast3.parse(src, filename, feature_version=feature_version) + except SyntaxError: + continue + if ast27.__name__ == "ast": + raise SyntaxError( + "The requested source code has invalid Python 3 syntax.\n" + "If you are trying to format Python 2 files please reinstall Black" + " with the 'python2' extra: `python3 -m pip install black[python2]`." + ) + return ast27.parse(src) + + +def stringify_ast( + node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0 +) -> Iterator[str]: + """Simple visitor generating strings to compare ASTs by content.""" + + node = fixup_ast_constants(node) + + 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 + + try: + value = getattr(node, field) + except AttributeError: + continue + + yield f"{' ' * (depth+1)}{field}=" + + if isinstance(value, list): + for item in value: + # Ignore nested tuples within del statements, because we may insert + # 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)) + ): + for item in item.elts: + yield from stringify_ast(item, depth + 2) + + 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)): + 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)) + ): + 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() + else: + normalized = value + 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, ast27.AST] +) -> Union[ast.AST, ast3.AST, ast27.AST]: + """Map ast nodes deprecated in 3.8 to Constant.""" + if isinstance(node, (ast.Str, ast3.Str, ast27.Str, ast.Bytes, ast3.Bytes)): + return ast.Constant(value=node.s) + + if isinstance(node, (ast.Num, ast3.Num, ast27.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/report.py b/src/black/report.py new file mode 100644 index 00000000000..8fc5da2e167 --- /dev/null +++ b/src/black/report.py @@ -0,0 +1,100 @@ +""" +Summarize Black runs to users. +""" +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + +from click import style + +from black.output import out, err + + +class Changed(Enum): + NO = 0 + CACHED = 1 + YES = 2 + + +@dataclass +class Report: + """Provides a reformatting counter. Can be rendered with `str(report)`.""" + + check: bool = False + diff: bool = False + quiet: bool = False + verbose: bool = False + change_count: int = 0 + same_count: int = 0 + failure_count: int = 0 + + def done(self, src: Path, changed: Changed) -> None: + """Increment the counter for successful reformatting. Write out a message.""" + if changed is Changed.YES: + reformatted = "would reformat" if self.check or self.diff else "reformatted" + if self.verbose or not self.quiet: + out(f"{reformatted} {src}") + self.change_count += 1 + else: + if self.verbose: + if changed is Changed.NO: + msg = f"{src} already well formatted, good job." + else: + msg = f"{src} wasn't modified on disk since last run." + out(msg, bold=False) + self.same_count += 1 + + def failed(self, src: Path, message: str) -> None: + """Increment the counter for failed reformatting. Write out a message.""" + err(f"error: cannot format {src}: {message}") + self.failure_count += 1 + + def path_ignored(self, path: Path, message: str) -> None: + if self.verbose: + out(f"{path} ignored: {message}", bold=False) + + @property + def return_code(self) -> int: + """Return the exit code that the app should use. + + This considers the current state of changed files and failures: + - if there were any failures, return 123; + - if any files were changed and --check is being used, return 1; + - otherwise return 0. + """ + # According to http://tldp.org/LDP/abs/html/exitcodes.html starting with + # 126 we have special return codes reserved by the shell. + if self.failure_count: + return 123 + + elif self.change_count and self.check: + return 1 + + return 0 + + def __str__(self) -> str: + """Render a color report of the current state. + + Use `click.unstyle` to remove colors. + """ + if self.check or self.diff: + reformatted = "would be reformatted" + unchanged = "would be left unchanged" + failed = "would fail to reformat" + else: + reformatted = "reformatted" + unchanged = "left unchanged" + failed = "failed to reformat" + report = [] + if self.change_count: + s = "s" if self.change_count > 1 else "" + report.append( + style(f"{self.change_count} file{s} {reformatted}", bold=True) + ) + if self.same_count: + s = "s" if self.same_count > 1 else "" + report.append(f"{self.same_count} file{s} {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")) + return ", ".join(report) + "." diff --git a/src/black/rusty.py b/src/black/rusty.py new file mode 100644 index 00000000000..822e3d7858a --- /dev/null +++ b/src/black/rusty.py @@ -0,0 +1,28 @@ +"""An error-handling model influenced by that used by the Rust programming language + +See https://doc.rust-lang.org/book/ch09-00-error-handling.html. +""" +from typing import Generic, TypeVar, Union + + +T = TypeVar("T") +E = TypeVar("E", bound=Exception) + + +class Ok(Generic[T]): + def __init__(self, value: T) -> None: + self._value = value + + def ok(self) -> T: + return self._value + + +class Err(Generic[E]): + def __init__(self, e: E) -> None: + self._e = e + + def err(self) -> E: + return self._e + + +Result = Union[Ok[T], Err[E]] diff --git a/src/black/strings.py b/src/black/strings.py new file mode 100644 index 00000000000..5b443ddebc9 --- /dev/null +++ b/src/black/strings.py @@ -0,0 +1,216 @@ +""" +Simple formatting on strings. Further string formatting code is in trans.py. +""" + +import regex as re +import sys +from typing import List, Pattern + + +STRING_PREFIX_CHARS = "furbFURB" # All possible string prefix characters. + + +def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: + """Replace `regex` with `replacement` twice on `original`. + + This is used by string normalization to perform replaces on + overlapping matches. + """ + return regex.sub(replacement, regex.sub(replacement, original)) + + +def has_triple_quotes(string: str) -> bool: + """ + Returns: + True iff @string starts with three quotation characters. + """ + raw_string = string.lstrip(STRING_PREFIX_CHARS) + return raw_string[:3] in {'"""', "'''"} + + +def lines_with_leading_tabs_expanded(s: str) -> List[str]: + """ + Splits string into lines and expands only leading tabs (following the normal + Python rules) + """ + lines = [] + 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) + if match: + first_non_whitespace_idx = match.start(1) + + lines.append( + line[:first_non_whitespace_idx].expandtabs() + + line[first_non_whitespace_idx:] + ) + else: + lines.append(line) + return lines + + +def fix_docstring(docstring: str, prefix: str) -> str: + # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation + if not docstring: + return "" + lines = lines_with_leading_tabs_expanded(docstring) + # Determine minimum indentation (first line doesn't count): + indent = sys.maxsize + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < sys.maxsize: + last_line_idx = len(lines) - 2 + for i, line in enumerate(lines[1:]): + stripped_line = line[indent:].rstrip() + if stripped_line or i == last_line_idx: + trimmed.append(prefix + stripped_line) + else: + trimmed.append("") + return "\n".join(trimmed) + + +def get_string_prefix(string: str) -> str: + """ + Pre-conditions: + * assert_is_leaf_string(@string) + + Returns: + @string's prefix (e.g. '', 'r', 'f', or 'rf'). + """ + assert_is_leaf_string(string) + + prefix = "" + prefix_idx = 0 + while string[prefix_idx] in STRING_PREFIX_CHARS: + prefix += string[prefix_idx].lower() + prefix_idx += 1 + + return prefix + + +def assert_is_leaf_string(string: str) -> None: + """ + Checks the pre-condition that @string has the format that you would expect + of `leaf.value` where `leaf` is some Leaf such that `leaf.type == + token.STRING`. A more precise description of the pre-conditions that are + checked are listed below. + + Pre-conditions: + * @string starts with either ', ", ', or " where + `set()` is some subset of `set(STRING_PREFIX_CHARS)`. + * @string ends with a quote character (' or "). + + Raises: + AssertionError(...) if the pre-conditions listed above are not + satisfied. + """ + dquote_idx = string.find('"') + squote_idx = string.find("'") + if -1 in [dquote_idx, squote_idx]: + quote_idx = max(dquote_idx, squote_idx) + else: + quote_idx = min(squote_idx, dquote_idx) + + assert ( + 0 <= quote_idx < len(string) - 1 + ), f"{string!r} is missing a starting quote character (' or \")." + assert string[-1] in ( + "'", + '"', + ), f"{string!r} is missing an ending quote character (' or \")." + assert set(string[:quote_idx]).issubset( + set(STRING_PREFIX_CHARS) + ), 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. + """ + match = re.match(r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", s, re.DOTALL) + 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", "") + return f"{new_prefix}{match.group(2)}" + + +def normalize_string_quotes(s: str) -> str: + """Prefer double quotes but only if it doesn't cause more escaping. + + Adds or removes backslashes as appropriate. Doesn't parse and fix + strings nested in f-strings. + """ + value = s.lstrip(STRING_PREFIX_CHARS) + if value[:3] == '"""': + return s + + elif value[:3] == "'''": + orig_quote = "'''" + new_quote = '"""' + elif value[0] == '"': + orig_quote = '"' + new_quote = "'" + else: + orig_quote = "'" + new_quote = '"' + first_quote_pos = s.find(orig_quote) + if first_quote_pos == -1: + 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}") + body = s[first_quote_pos + len(orig_quote) : -len(orig_quote)] + if "r" in prefix.casefold(): + if unescaped_new_quote.search(body): + # There's at least one unescaped new_quote in this raw string + # so converting is impossible + return s + + # Do not introduce or remove backslashes in raw strings + new_body = body + else: + # remove unnecessary escapes + new_body = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", body) + if body != new_body: + # Consider the string without unnecessary escapes as the original + body = new_body + s = f"{prefix}{orig_quote}{body}{orig_quote}" + new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body) + new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body) + if "f" in prefix.casefold(): + matches = re.findall( + r""" + (?:[^{]|^)\{ # start of the string or a non-{ followed by a single { + ([^{].*?) # contents of the brackets except if begins with {{ + \}(?:[^}]|$) # A } followed by end of the string or a non-} + """, + new_body, + re.VERBOSE, + ) + for m in matches: + if "\\" in str(m): + # Do not introduce backslashes in interpolated expressions + return s + + if new_quote == '"""' and new_body[-1:] == '"': + # edge case: + new_body = new_body[:-1] + '\\"' + orig_escape_count = body.count("\\") + new_escape_count = new_body.count("\\") + if new_escape_count > orig_escape_count: + return s # Do not introduce more escaping + + if new_escape_count == orig_escape_count and orig_quote == '"': + return s # Prefer double quotes + + return f"{prefix}{new_quote}{new_body}{new_quote}" diff --git a/src/black/trans.py b/src/black/trans.py new file mode 100644 index 00000000000..055f33c0ee2 --- /dev/null +++ b/src/black/trans.py @@ -0,0 +1,1925 @@ +""" +String transformers that can split and merge strings. +""" +from abc import ABC, abstractmethod +from collections import defaultdict +from dataclasses import dataclass +import regex as re +from typing import ( + Any, + Callable, + Collection, + Dict, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) + +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 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 blib2to3.pgen2 import token + + +class CannotTransform(Exception): + """Base class for errors raised by Transformers.""" + + +# types +T = TypeVar("T") +LN = Union[Leaf, Node] +Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]] +Index = int +NodeType = int +ParserState = int +StringID = int +TResult = Result[T, CannotTransform] # (T)ransform Result +TMatchResult = TResult[Index] + + +def TErr(err_msg: str) -> Err[CannotTransform]: + """(T)ransform Err + + Convenience function used when working with the TResult type. + """ + cant_transform = CannotTransform(err_msg) + return Err(cant_transform) + + +@dataclass # type: ignore +class StringTransformer(ABC): + """ + An implementation of the Transformer protocol that relies on its + subclasses overriding the template methods `do_match(...)` and + `do_transform(...)`. + + This Transformer works exclusively on strings (for example, by merging + or splitting them). + + The following sections can be found among the docstrings of each concrete + StringTransformer subclass. + + Requirements: + Which requirements must be met of the given Line for this + StringTransformer to be applied? + + Transformations: + If the given Line meets all of the above requirements, which string + transformations can you expect to be applied to it by this + StringTransformer? + + Collaborations: + What contractual agreements does this StringTransformer have with other + StringTransfomers? Such collaborations should be eliminated/minimized + as much as possible. + """ + + line_length: int + normalize_strings: bool + __name__ = "StringTransformer" + + @abstractmethod + 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. + OR + * Err(CannotTransform), if a match was not able to be made. + """ + + @abstractmethod + def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + """ + Yields: + * Ok(new_line) where new_line is the new transformed line. + 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. + + Side Effects: + This method should NOT mutate @line directly, but it MAY mutate the + Line's underlying Node structure. (WARNING: If the underlying Node + structure IS altered, then this method should NOT be allowed to + yield an CannotTransform after that point.) + """ + + def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]: + """ + StringTransformer instances have a call signature that mirrors that of + the Transformer type. + + Raises: + CannotTransform(...) if the concrete StringTransformer class is unable + to transform @line. + """ + # Optimization to avoid calling `self.do_match(...)` when the line does + # not contain any string. + if not any(leaf.type == token.STRING for leaf in line.leaves): + raise CannotTransform("There are no strings in this line.") + + match_result = self.do_match(line) + + if isinstance(match_result, Err): + cant_transform = match_result.err() + raise CannotTransform( + f"The string transformer {self.__class__.__name__} does not recognize" + " this line as one that it can transform." + ) from cant_transform + + string_idx = match_result.ok() + + for line_result in self.do_transform(line, string_idx): + if isinstance(line_result, Err): + cant_transform = line_result.err() + raise CannotTransform( + "StringTransformer failed while attempting to transform string." + ) from cant_transform + line = line_result.ok() + yield line + + +@dataclass +class CustomSplit: + """A custom (i.e. manual) string split. + + A single CustomSplit instance represents a single substring. + + Examples: + Consider the following string: + ``` + "Hi there friend." + " This is a custom" + f" string {split}." + ``` + + This string will correspond to the following three CustomSplit instances: + ``` + CustomSplit(False, 16) + CustomSplit(False, 17) + CustomSplit(True, 16) + ``` + """ + + has_prefix: bool + break_idx: int + + +class CustomSplitMapMixin: + """ + This mixin class is used to map merged strings to a sequence of + CustomSplits, which will then be used to re-split the strings iff none of + the resultant substrings go over the configured max line length. + """ + + _Key = Tuple[StringID, str] + _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple) + + @staticmethod + def _get_key(string: str) -> "CustomSplitMapMixin._Key": + """ + Returns: + A unique identifier that is used internally to map @string to a + group of custom splits. + """ + return (id(string), string) + + def add_custom_splits( + self, string: str, custom_splits: Iterable[CustomSplit] + ) -> None: + """Custom Split Map Setter Method + + Side Effects: + Adds a mapping from @string to the custom splits @custom_splits. + """ + key = self._get_key(string) + self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits) + + def pop_custom_splits(self, string: str) -> List[CustomSplit]: + """Custom Split Map Getter Method + + Returns: + * A list of the custom splits that are mapped to @string, if any + exist. + OR + * [], otherwise. + + Side Effects: + Deletes the mapping between @string and its associated custom + splits (which are returned to the caller). + """ + key = self._get_key(string) + + custom_splits = self._CUSTOM_SPLIT_MAP[key] + del self._CUSTOM_SPLIT_MAP[key] + + return list(custom_splits) + + def has_custom_splits(self, string: str) -> bool: + """ + Returns: + True iff @string is associated with a set of custom splits. + """ + key = self._get_key(string) + return key in self._CUSTOM_SPLIT_MAP + + +class StringMerger(CustomSplitMapMixin, StringTransformer): + """StringTransformer that merges strings together. + + Requirements: + (A) The line contains adjacent strings such that ALL of the validation checks + listed in StringMerger.__validate_msg(...)'s docstring pass. + 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 + (B) All line-continuation backslashes are removed from the target string. + + Collaborations: + StringMerger provides custom split information to StringSplitter. + """ + + def do_match(self, line: Line) -> TMatchResult: + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + + for (i, leaf) in enumerate(LL): + if ( + leaf.type == token.STRING + and is_valid_index(i + 1) + and LL[i + 1].type == token.STRING + ): + return Ok(i) + + if leaf.type == token.STRING and "\\\n" in leaf.value: + return Ok(i) + + return TErr("This line has no strings that need merging.") + + def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + new_line = line + rblc_result = self._remove_backslash_line_continuation_chars( + new_line, string_idx + ) + if isinstance(rblc_result, Ok): + new_line = rblc_result.ok() + + msg_result = self._merge_string_group(new_line, string_idx) + if isinstance(msg_result, Ok): + new_line = msg_result.ok() + + if isinstance(rblc_result, Err) and isinstance(msg_result, Err): + msg_cant_transform = msg_result.err() + rblc_cant_transform = rblc_result.err() + cant_transform = CannotTransform( + "StringMerger failed to merge any strings in this line." + ) + + # Chain the errors together using `__cause__`. + msg_cant_transform.__cause__ = rblc_cant_transform + cant_transform.__cause__ = msg_cant_transform + + yield Err(cant_transform) + else: + yield Ok(new_line) + + @staticmethod + def _remove_backslash_line_continuation_chars( + line: Line, string_idx: int + ) -> TResult[Line]: + """ + Merge strings that were split across multiple lines using + line-continuation backslashes. + + Returns: + Ok(new_line), if @line contains backslash line-continuation + characters. + OR + Err(CannotTransform), otherwise. + """ + 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) + ): + return TErr( + f"String leaf {string_leaf} does not contain any 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", "") + + return Ok(new_line) + + def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]: + """ + Merges string group (i.e. set of adjacent strings) where the first + string in the group is `line.leaves[string_idx]`. + + Returns: + Ok(new_line), if ALL of the validation checks found in + __validate_msg(...) pass. + OR + Err(CannotTransform), otherwise. + """ + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + + vresult = self._validate_msg(line, string_idx) + if isinstance(vresult, Err): + return vresult + + # 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 + + # We will place BREAK_MARK in between every two substrings that we + # merge. We will then later go through our final result and use the + # various instances of BREAK_MARK we find to add the right values to + # the custom split map. + BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@" + + QUOTE = LL[string_idx].value[-1] + + def make_naked(string: str, string_prefix: str) -> str: + """Strip @string (i.e. make it a "naked" string) + + Pre-conditions: + * assert_is_leaf_string(@string) + + Returns: + A string that is identical to @string except that + @string_prefix has been stripped, the surrounding QUOTE + characters have been removed, and any remaining QUOTE + characters have been escaped. + """ + assert_is_leaf_string(string) + + RE_EVEN_BACKSLASHES = r"(?:(?= 0 + ), "Logic error while filling the custom string breakpoint cache." + + temp_string = temp_string[mark_idx + len(BREAK_MARK) :] + breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1 + custom_splits.append(CustomSplit(has_prefix, breakpoint_idx)) + + string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, "")) + + if atom_node is not None: + 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) + + @staticmethod + 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(...). + + Returns: + * Ok(None), if ALL validation checks (listed below) pass. + OR + * Err(CannotTransform), if any of the following are true: + - The target string group does not contain ANY stand-alone comments. + - The target string is not in a string group (i.e. it has no + adjacent strings). + - The string group has more than one inline comment. + - The string group has an inline comment that appears to be a pragma. + - 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. + """ + # We first check for "inner" stand-alone comments (i.e. stand-alone + # comments that have a string leaf before them AND after them). + for inc in [1, -1]: + i = string_idx + found_sa_comment = False + is_valid_index = is_valid_index_factory(line.leaves) + while is_valid_index(i) and line.leaves[i].type in [ + token.STRING, + STANDALONE_COMMENT, + ]: + if line.leaves[i].type == STANDALONE_COMMENT: + found_sa_comment = True + elif found_sa_comment: + return TErr( + "StringMerger does NOT merge string groups which contain " + "stand-alone comments." + ) + + i += inc + + num_of_inline_string_comments = 0 + set_of_prefixes = set() + num_of_strings = 0 + for leaf in line.leaves[string_idx:]: + if leaf.type != token.STRING: + # If the string group is trailed by a comma, we count the + # comments trailing the comma to be one of the string group's + # comments. + if leaf.type == token.COMMA and id(leaf) in line.comments: + num_of_inline_string_comments += 1 + break + + if has_triple_quotes(leaf.value): + return TErr("StringMerger does NOT merge multiline strings.") + + num_of_strings += 1 + prefix = get_string_prefix(leaf.value) + if "r" in prefix: + return TErr("StringMerger does NOT merge raw strings.") + + set_of_prefixes.add(prefix) + + if id(leaf) in line.comments: + num_of_inline_string_comments += 1 + if contains_pragma_comment(line.comments[id(leaf)]): + return TErr("Cannot merge strings which have pragma comments.") + + if num_of_strings < 2: + return TErr( + f"Not enough strings to merge (num_of_strings={num_of_strings})." + ) + + if num_of_inline_string_comments > 1: + return TErr( + f"Too many inline string comments ({num_of_inline_string_comments})." + ) + + if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}: + return TErr(f"Too many different prefixes ({set_of_prefixes}).") + + return Ok(None) + + +class StringParenStripper(StringTransformer): + """StringTransformer that strips surrounding parentheses from strings. + + Requirements: + The line contains a string which is surrounded by parentheses and: + - The target string is NOT the only argument to a function call. + - The target string is NOT a "pointless" string. + - If the target string contains a PERCENT, the brackets are not + preceeded or followed by an operator with higher precedence than + PERCENT. + + Transformations: + The parentheses mentioned in the 'Requirements' section are stripped. + + Collaborations: + StringParenStripper has its own inherent usefulness, but it is also + relied on to clean up the parentheses created by StringParenWrapper (in + the event that they are no longer needed). + """ + + def do_match(self, line: Line) -> TMatchResult: + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + + for (idx, leaf) in enumerate(LL): + # Should be a string... + if leaf.type != token.STRING: + continue + + # If this is a "pointless" string... + if ( + leaf.parent + and leaf.parent.parent + and leaf.parent.parent.type == syms.simple_stmt + ): + continue + + # Should be preceded by a non-empty LPAR... + if ( + not is_valid_index(idx - 1) + or LL[idx - 1].type != token.LPAR + or is_empty_lpar(LL[idx - 1]) + ): + continue + + # That LPAR should NOT be preceded by a function name or a closing + # bracket (which could be a function which returns a function or a + # list/dictionary that contains a function)... + if is_valid_index(idx - 2) and ( + LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS + ): + continue + + string_idx = idx + + # Skip the string trailer, if one exists. + string_parser = StringParser() + next_idx = string_parser.parse(LL, string_idx) + + # if the leaves in the parsed string include a PERCENT, we need to + # make sure the initial LPAR is NOT preceded by an operator with + # higher or equal precedence to PERCENT + if is_valid_index(idx - 2): + # mypy can't quite follow unless we name this + before_lpar = LL[idx - 2] + if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and ( + ( + before_lpar.type + in { + token.STAR, + token.AT, + token.SLASH, + token.DOUBLESLASH, + token.PERCENT, + token.TILDE, + token.DOUBLESTAR, + token.AWAIT, + token.LSQB, + token.LPAR, + } + ) + or ( + # only unary PLUS/MINUS + before_lpar.parent + and before_lpar.parent.type == syms.factor + and (before_lpar.type in {token.PLUS, token.MINUS}) + ) + ): + continue + + # Should be followed by a non-empty RPAR... + if ( + is_valid_index(next_idx) + and LL[next_idx].type == token.RPAR + and not is_empty_rpar(LL[next_idx]) + ): + # That RPAR should NOT be followed by anything with higher + # precedence than PERCENT + if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { + token.DOUBLESTAR, + token.LSQB, + token.LPAR, + token.DOT, + }: + continue + + return Ok(string_idx) + + return TErr("This line has no strings wrapped in parens.") + + def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + LL = line.leaves + + string_parser = StringParser() + rpar_idx = string_parser.parse(LL, string_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 + + 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) + + 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) + + append_leaves( + new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] + ) + + LL[rpar_idx].remove() + + yield Ok(new_line) + + +class BaseStringSplitter(StringTransformer): + """ + Abstract class for StringTransformers which transform a Line's strings by splitting + them or placing them on their own lines where necessary to avoid going over + the configured line length. + + 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 + * The target string is NOT a "pointless" string (i.e. a string that has + no parent or siblings). + AND + * The target string is not followed by an inline comment that appears + to be a pragma. + AND + * The target string is not a multiline (i.e. triple-quote) string. + """ + + @abstractmethod + def do_splitter_match(self, line: Line) -> TMatchResult: + """ + BaseStringSplitter asks its clients to override this method instead of + `StringTransformer.do_match(...)`. + + Follows the same protocol as `StringTransformer.do_match(...)`. + + Refer to `help(StringTransformer.do_match)` for more information. + """ + + def do_match(self, line: Line) -> TMatchResult: + match_result = self.do_splitter_match(line) + if isinstance(match_result, Err): + return match_result + + string_idx = match_result.ok() + vresult = self._validate(line, string_idx) + if isinstance(vresult, Err): + return vresult + + return match_result + + def _validate(self, line: Line, string_idx: int) -> TResult[None]: + """ + Checks that @line meets all of the requirements listed in this classes' + docstring. Refer to `help(BaseStringSplitter)` for a detailed + description of those requirements. + + Returns: + * Ok(None), if ALL of the requirements are met. + OR + * Err(CannotTransform), if ANY of the requirements are NOT met. + """ + LL = line.leaves + + string_leaf = LL[string_idx] + + max_string_length = self._get_max_string_length(line, string_idx) + if len(string_leaf.value) <= max_string_length: + return TErr( + "The string itself is not what is causing this line to be too long." + ) + + if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [ + token.STRING, + token.NEWLINE, + ]: + return TErr( + f"This string ({string_leaf.value}) appears to be pointless (i.e. has" + " no parent)." + ) + + if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment( + line.comments[id(line.leaves[string_idx])] + ): + return TErr( + "Line appears to end with an inline pragma comment. Splitting the line" + " could modify the pragma's behavior." + ) + + if has_triple_quotes(string_leaf.value): + return TErr("We cannot split multiline strings.") + + return Ok(None) + + def _get_max_string_length(self, line: Line, string_idx: int) -> int: + """ + Calculates the max string length used when attempting to determine + whether or not the target string is responsible for causing the line to + go over the line length limit. + + WARNING: This method is tightly coupled to both StringSplitter and + (especially) StringParenWrapper. There is probably a better way to + accomplish what is being done here. + + Returns: + max_string_length: such that `line.leaves[string_idx].value > + max_string_length` implies that the target string IS responsible + for causing this line to exceed the line length limit. + """ + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + + # We use the shorthand "WMA4" in comments to abbreviate "We must + # account for". When giving examples, we use STRING to mean some/any + # valid string. + # + # Finally, we use the following convenience variables: + # + # P: The leaf that is before the target string leaf. + # N: The leaf that is after the target string leaf. + # NN: The leaf that is after N. + + # WMA4 the whitespace at the beginning of the line. + offset = line.depth * 4 + + if is_valid_index(string_idx - 1): + p_idx = string_idx - 1 + if ( + LL[string_idx - 1].type == token.LPAR + and LL[string_idx - 1].value == "" + and string_idx >= 2 + ): + # If the previous leaf is an empty LPAR placeholder, we should skip it. + p_idx -= 1 + + P = LL[p_idx] + if P.type == token.PLUS: + # WMA4 a space and a '+' character (e.g. `+ STRING`). + offset += 2 + + if P.type == token.COMMA: + # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`]. + offset += 3 + + if P.type in [token.COLON, token.EQUAL, token.NAME]: + # This conditional branch is meant to handle dictionary keys, + # variable assignments, 'return STRING' statement lines, and + # 'else STRING' ternary expression lines. + + # WMA4 a single space. + offset += 1 + + # WMA4 the lengths of any leaves that came before that space, + # but after any closing bracket before that space. + for leaf in reversed(LL[: p_idx + 1]): + offset += len(str(leaf)) + if leaf.type in CLOSING_BRACKETS: + break + + if is_valid_index(string_idx + 1): + N = LL[string_idx + 1] + if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2: + # If the next leaf is an empty RPAR placeholder, we should skip it. + N = LL[string_idx + 2] + + if N.type == token.COMMA: + # WMA4 a single comma at the end of the string (e.g `STRING,`). + offset += 1 + + if is_valid_index(string_idx + 2): + NN = LL[string_idx + 2] + + if N.type == token.DOT and NN.type == token.NAME: + # This conditional branch is meant to handle method calls invoked + # off of a string literal up to and including the LPAR character. + + # WMA4 the '.' character. + offset += 1 + + if ( + is_valid_index(string_idx + 3) + and LL[string_idx + 3].type == token.LPAR + ): + # WMA4 the left parenthesis character. + offset += 1 + + # WMA4 the length of the method's name. + offset += len(NN.value) + + has_comments = False + for comment_leaf in line.comments_after(LL[string_idx]): + if not has_comments: + has_comments = True + # WMA4 two spaces before the '#' character. + offset += 2 + + # WMA4 the length of the inline comment. + offset += len(comment_leaf.value) + + max_string_length = self.line_length - offset + return max_string_length + + +class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): + """ + StringTransformer that splits "atom" strings (i.e. strings which exist on + lines by themselves). + + Requirements: + * The line consists ONLY of a single string (with the exception of a + '+' symbol which MAY exist at the start of the line), MAYBE a string + trailer, and MAYBE a trailing comma. + AND + * All of the requirements listed in BaseStringSplitter's docstring. + + Transformations: + The string mentioned in the 'Requirements' section is split into as + many substrings as necessary to adhere to the configured line length. + + In the final set of substrings, no substring should be smaller than + MIN_SUBSTR_SIZE characters. + + The string will ONLY be split on spaces (i.e. each new substring should + start with a space). Note that the string will NOT be split on a space + which is escaped with a backslash. + + If the string is an f-string, it will NOT be split in the middle of an + f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x + else bar()} is an f-expression). + + If the string that is being split has an associated set of custom split + records and those custom splits will NOT result in any line going over + the configured line length, those custom splits are used. Otherwise the + string is split as late as possible (from left-to-right) while still + adhering to the transformation rules listed above. + + Collaborations: + StringSplitter relies on StringMerger to construct the appropriate + CustomSplit objects and add them to the custom split map. + """ + + MIN_SUBSTR_SIZE = 6 + # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. + RE_FEXPR = r""" + (? TMatchResult: + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + + idx = 0 + + # The first leaf MAY be a '+' symbol... + if is_valid_index(idx) and LL[idx].type == token.PLUS: + idx += 1 + + # The next/first leaf MAY be an empty LPAR... + if is_valid_index(idx) and is_empty_lpar(LL[idx]): + idx += 1 + + # The next/first leaf MUST be a string... + if not is_valid_index(idx) or LL[idx].type != token.STRING: + return TErr("Line does not start with a string.") + + string_idx = idx + + # Skip the string trailer, if one exists. + string_parser = StringParser() + idx = string_parser.parse(LL, string_idx) + + # That string MAY be followed by an empty RPAR... + if is_valid_index(idx) and is_empty_rpar(LL[idx]): + idx += 1 + + # That string / empty RPAR leaf MAY be followed by a comma... + if is_valid_index(idx) and LL[idx].type == token.COMMA: + idx += 1 + + # But no more leaves are allowed... + if is_valid_index(idx): + return TErr("This line does not end with a string.") + + return Ok(string_idx) + + def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + LL = line.leaves + + QUOTE = LL[string_idx].value[-1] + + is_valid_index = is_valid_index_factory(LL) + insert_str_child = insert_str_child_factory(LL[string_idx]) + + prefix = get_string_prefix(LL[string_idx].value) + + # We MAY choose to drop the 'f' prefix from substrings that don't + # 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 + ) + + first_string_line = True + starts_with_plus = LL[0].type == token.PLUS + + def line_needs_plus() -> bool: + return first_string_line and starts_with_plus + + def maybe_append_plus(new_line: Line) -> None: + """ + Side Effects: + If @line starts with a plus and this is the first line we are + constructing, this function appends a PLUS leaf to @new_line + and replaces the old PLUS leaf in the node structure. Otherwise + this function does nothing. + """ + if line_needs_plus(): + plus_leaf = Leaf(token.PLUS, "+") + replace_child(LL[0], plus_leaf) + new_line.append(plus_leaf) + + ends_with_comma = ( + is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA + ) + + def max_last_string() -> int: + """ + Returns: + The max allowed length of the string value used for the last + line we will construct. + """ + result = self.line_length + result -= line.depth * 4 + result -= 1 if ends_with_comma else 0 + result -= 2 if line_needs_plus() else 0 + return result + + # --- Calculate Max Break Index (for string value) + # We start with the line length limit + max_break_idx = self.line_length + # The last index of a string of length N is N-1. + max_break_idx -= 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: + yield TErr( + f"Unable to split {LL[string_idx].value} at such high of a line depth:" + f" {line.depth}" + ) + return + + # Check if StringMerger registered any custom splits. + custom_splits = self.pop_custom_splits(LL[string_idx].value) + # We use them ONLY if none of them would produce lines that exceed the + # line limit. + use_custom_breakpoints = bool( + custom_splits + and all(csplit.break_idx <= max_break_idx for csplit in custom_splits) + ) + + # Temporary storage for the remaining chunk of the string line that + # can't fit onto the line currently being constructed. + rest_value = LL[string_idx].value + + def more_splits_should_be_made() -> bool: + """ + Returns: + True iff `rest_value` (the remaining string value from the last + split), should be split again. + """ + if use_custom_breakpoints: + return len(custom_splits) > 1 + else: + return len(rest_value) > max_last_string() + + string_line_results: List[Ok[Line]] = [] + while more_splits_should_be_made(): + if use_custom_breakpoints: + # Custom User Split (manual) + csplit = custom_splits.pop(0) + break_idx = csplit.break_idx + else: + # Algorithmic Split (automatic) + max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx + 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 + # and this string has custom splits registered to it, we + # fall back to using them--which means we have to start + # over from the beginning. + if custom_splits: + rest_value = LL[string_idx].value + string_line_results = [] + first_string_line = True + use_custom_breakpoints = True + continue + + # Otherwise, we stop splitting here. + break + + break_idx = maybe_break_idx + + # --- Construct `next_value` + next_value = rest_value[:break_idx] + QUOTE + if ( + # Are we allowed to try to drop a pointless 'f' prefix? + drop_pointless_f_prefix + # If we are, will we be successful? + and next_value != self._normalize_f_string(next_value, prefix) + ): + # If the current custom split did NOT originally use a prefix, + # then `csplit.break_idx` will be off by one after removing + # the 'f' prefix. + break_idx = ( + break_idx + 1 + if use_custom_breakpoints and not csplit.has_prefix + else break_idx + ) + next_value = rest_value[:break_idx] + QUOTE + next_value = self._normalize_f_string(next_value, prefix) + + # --- Construct `next_leaf` + next_leaf = Leaf(token.STRING, next_value) + insert_str_child(next_leaf) + self._maybe_normalize_string_quotes(next_leaf) + + # --- Construct `next_line` + next_line = line.clone() + maybe_append_plus(next_line) + next_line.append(next_leaf) + string_line_results.append(Ok(next_line)) + + rest_value = prefix + QUOTE + rest_value[break_idx:] + first_string_line = False + + yield from string_line_results + + if drop_pointless_f_prefix: + rest_value = self._normalize_f_string(rest_value, prefix) + + rest_leaf = Leaf(token.STRING, rest_value) + insert_str_child(rest_leaf) + + # NOTE: I could not find a test case that verifies that the following + # line is actually necessary, but it seems to be. Otherwise we risk + # not normalizing the last substring, right? + self._maybe_normalize_string_quotes(rest_leaf) + + last_line = line.clone() + maybe_append_plus(last_line) + + # If there are any leaves to the right of the target string... + if is_valid_index(string_idx + 1): + # We use `temp_value` here to determine how long the last line + # would be if we were to append all the leaves to the right of the + # target string to the last string line. + temp_value = rest_value + for leaf in LL[string_idx + 1 :]: + temp_value += str(leaf) + if leaf.type == token.LPAR: + break + + # Try to fit them all on the same line with the last substring... + if ( + len(temp_value) <= max_last_string() + or LL[string_idx + 1].type == token.COMMA + ): + last_line.append(rest_leaf) + append_leaves(last_line, line, LL[string_idx + 1 :]) + yield Ok(last_line) + # Otherwise, place the last substring on one line and everything + # else on a line below that... + else: + last_line.append(rest_leaf) + yield Ok(last_line) + + non_string_line = line.clone() + append_leaves(non_string_line, line, LL[string_idx + 1 :]) + yield Ok(non_string_line) + # Else the target string was the last leaf... + else: + last_line.append(rest_leaf) + last_line.comments = line.comments.copy() + yield Ok(last_line) + + def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]: + """ + This method contains the algorithm that StringSplitter uses to + determine which character to split each string at. + + Args: + @string: The substring that we are attempting to split. + @max_break_idx: The ideal break index. We will return this value if it + meets all the necessary conditions. In the likely event that it + doesn't we will try to find the closest index BELOW @max_break_idx + that does. If that fails, we will expand our search by also + considering all valid indices ABOVE @max_break_idx. + + Pre-Conditions: + * assert_is_leaf_string(@string) + * 0 <= @max_break_idx < len(@string) + + Returns: + break_idx, if an index is able to be found that meets all of the + conditions listed in the 'Transformations' section of this classes' + docstring. + OR + None, otherwise. + """ + is_valid_index = is_valid_index_factory(string) + + assert is_valid_index(max_break_idx) + assert_is_leaf_string(string) + + _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None + + def fexpr_slices() -> Iterator[Tuple[Index, Index]]: + """ + Yields: + All ranges of @string which, if @string were to be split there, + would result in the splitting of an f-expression (which is NOT + allowed). + """ + nonlocal _fexpr_slices + + if _fexpr_slices is None: + _fexpr_slices = [] + for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE): + _fexpr_slices.append(match.span()) + + yield from _fexpr_slices + + is_fstring = "f" in get_string_prefix(string) + + def breaks_fstring_expression(i: Index) -> bool: + """ + Returns: + True iff returning @i would result in the splitting of an + f-expression (which is NOT allowed). + """ + if not is_fstring: + return False + + for (start, end) in fexpr_slices(): + if start <= i < end: + return True + + return False + + 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. + """ + is_space = string[i] == " " + + is_not_escaped = True + j = i - 1 + while is_valid_index(j) and string[j] == "\\": + is_not_escaped = not is_not_escaped + j -= 1 + + is_big_enough = ( + len(string[i:]) >= self.MIN_SUBSTR_SIZE + and len(string[:i]) >= self.MIN_SUBSTR_SIZE + ) + return ( + is_space + and is_not_escaped + and is_big_enough + and not breaks_fstring_expression(i) + ) + + # First, we check all indices BELOW @max_break_idx. + break_idx = max_break_idx + while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx): + break_idx -= 1 + + if not passes_all_checks(break_idx): + # If that fails, we check all indices ABOVE @max_break_idx. + # + # If we are able to find a valid index here, the next line is going + # to be longer than the specified line length, but it's probably + # better than doing nothing at all. + break_idx = max_break_idx + 1 + while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx): + break_idx += 1 + + if not is_valid_index(break_idx) or not passes_all_checks(break_idx): + return None + + return break_idx + + def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None: + if self.normalize_strings: + leaf.value = normalize_string_quotes(leaf.value) + + def _normalize_f_string(self, string: str, prefix: str) -> str: + """ + Pre-Conditions: + * assert_is_leaf_string(@string) + + Returns: + * If @string is an f-string that contains no f-expressions, we + return a string identical to @string except that the 'f' prefix + has been stripped and all double braces (i.e. '{{' or '}}') have + been normalized (i.e. turned into '{' or '}'). + OR + * Otherwise, we return @string. + """ + assert_is_leaf_string(string) + + if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE): + new_prefix = prefix.replace("f", "") + + temp = string[len(prefix) :] + temp = re.sub(r"\{\{", "{", temp) + temp = re.sub(r"\}\}", "}", temp) + new_string = temp + + return f"{new_prefix}{new_string}" + else: + return string + + +class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): + """ + StringTransformer that splits non-"atom" strings (i.e. strings that do not + exist on lines by themselves). + + Requirements: + All of the requirements listed in BaseStringSplitter's docstring in + addition to the requirements listed below: + + * The line is a return/yield statement, which returns/yields a string. + 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 + * The line is an assert statement, which ends with a string. + 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 + * The line is a dictionary key assignment where some valid key is being + assigned the value of some string. + + Transformations: + The chosen string is wrapped in parentheses and then split at the LPAR. + + We then have one line which ends with an LPAR and another line that + starts with the chosen string. The latter line is then split again at + the RPAR. This results in the RPAR (and possibly a trailing comma) + being placed on its own line. + + NOTE: If any leaves exist to the right of the chosen string (except + for a trailing comma, which would be placed after the RPAR), those + leaves are placed inside the parentheses. In effect, the chosen + string is not necessarily being "wrapped" by parentheses. We can, + however, count on the LPAR being placed directly before the chosen + string. + + In other words, StringParenWrapper creates "atom" strings. These + can then be split again by StringSplitter, if necessary. + + Collaborations: + In the event that a string line split by StringParenWrapper is + changed such that it no longer needs to be given its own line, + StringParenWrapper relies on StringParenStripper to clean up the + parentheses it created. + """ + + def do_splitter_match(self, line: Line) -> TMatchResult: + LL = line.leaves + + string_idx = ( + self._return_match(LL) + or self._else_match(LL) + or self._assert_match(LL) + or self._assign_match(LL) + or self._dict_match(LL) + ) + + if string_idx is not None: + string_value = line.leaves[string_idx].value + # If the string has no spaces... + if " " not 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: + # 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. + return TErr( + "We do not wrap long strings in parentheses when the" + " resultant line would still be over the specified line" + " length and can't be split further by StringSplitter." + ) + return Ok(string_idx) + + return TErr("This line does not contain any non-atomic strings.") + + @staticmethod + def _return_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 return/yield statement + requirements listed in the 'Requirements' section of this classes' + docstring. + OR + None, otherwise. + """ + # If this line is apart 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 + ].value in ["return", "yield"]: + is_valid_index = is_valid_index_factory(LL) + + idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 + # The next visible leaf MUST contain a string... + if is_valid_index(idx) and LL[idx].type == token.STRING: + return idx + + return None + + @staticmethod + def _else_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 ternary expression + requirements listed in the 'Requirements' section of this classes' + docstring. + OR + None, otherwise. + """ + # If this line is apart of a ternary expression and the first leaf + # contains the "else" keyword... + if ( + parent_type(LL[0]) == syms.test + and LL[0].type == token.NAME + and LL[0].value == "else" + ): + is_valid_index = is_valid_index_factory(LL) + + idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 + # The next visible leaf MUST contain a string... + if is_valid_index(idx) and LL[idx].type == token.STRING: + return idx + + return None + + @staticmethod + def _assert_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 assert statement + requirements listed in the 'Requirements' section of this classes' + docstring. + OR + None, otherwise. + """ + # If this line is apart 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) + + 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 + + # That comma MUST be followed by a string... + if is_valid_index(idx) and LL[idx].type == token.STRING: + string_idx = idx + + # Skip the string trailer, if one exists. + string_parser = StringParser() + idx = string_parser.parse(LL, string_idx) + + # But no more leaves are allowed... + if not is_valid_index(idx): + return string_idx + + return None + + @staticmethod + def _assign_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 assignment statement + requirements listed in the 'Requirements' section of this classes' + docstring. + OR + None, otherwise. + """ + # If this line is apart 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] + and LL[0].type == token.NAME + ): + is_valid_index = is_valid_index_factory(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 + + # That symbol MUST be followed by a string... + if is_valid_index(idx) and LL[idx].type == token.STRING: + string_idx = idx + + # Skip the string trailer, if one exists. + string_parser = StringParser() + idx = string_parser.parse(LL, string_idx) + + # The next leaf MAY be a comma iff this line is apart + # of a function argument... + if ( + parent_type(LL[0]) == syms.argument + and is_valid_index(idx) + and LL[idx].type == token.COMMA + ): + idx += 1 + + # But no more leaves are allowed... + if not is_valid_index(idx): + return string_idx + + return None + + @staticmethod + def _dict_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. + 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)]: + is_valid_index = is_valid_index_factory(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 + + # That colon MUST be followed by a string... + if is_valid_index(idx) and LL[idx].type == token.STRING: + string_idx = idx + + # Skip the string trailer, if one exists. + string_parser = StringParser() + idx = string_parser.parse(LL, string_idx) + + # That string MAY be followed by a comma... + if is_valid_index(idx) and LL[idx].type == token.COMMA: + idx += 1 + + # But no more leaves are allowed... + if not is_valid_index(idx): + return string_idx + + return None + + def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: + LL = line.leaves + + is_valid_index = is_valid_index_factory(LL) + insert_str_child = insert_str_child_factory(LL[string_idx]) + + comma_idx = -1 + ends_with_comma = False + if LL[comma_idx].type == token.COMMA: + ends_with_comma = True + + leaves_to_steal_comments_from = [LL[string_idx]] + if ends_with_comma: + leaves_to_steal_comments_from.append(LL[comma_idx]) + + # --- First Line + first_line = line.clone() + left_leaves = LL[:string_idx] + + # We have to remember to account for (possibly invisible) LPAR and RPAR + # leaves that already wrapped the target string. If these leaves do + # exist, we will replace them with our own LPAR and RPAR leaves. + old_parens_exist = False + if left_leaves and left_leaves[-1].type == token.LPAR: + old_parens_exist = True + leaves_to_steal_comments_from.append(left_leaves[-1]) + left_leaves.pop() + + append_leaves(first_line, line, left_leaves) + + lpar_leaf = Leaf(token.LPAR, "(") + if old_parens_exist: + replace_child(LL[string_idx - 1], lpar_leaf) + else: + insert_str_child(lpar_leaf) + first_line.append(lpar_leaf) + + # We throw inline comments that were originally to the right of the + # target string to the top line. They will now be shown to the right of + # the LPAR. + for leaf in leaves_to_steal_comments_from: + for comment_leaf in line.comments_after(leaf): + first_line.append(comment_leaf, preformatted=True) + + yield Ok(first_line) + + # --- Middle (String) Line + # We only need to yield one (possibly too long) string line, since the + # `StringSplitter` will break it down further if necessary. + string_value = LL[string_idx].value + string_line = Line( + mode=line.mode, + depth=line.depth + 1, + inside_brackets=True, + should_split_rhs=line.should_split_rhs, + magic_trailing_comma=line.magic_trailing_comma, + ) + string_leaf = Leaf(token.STRING, string_value) + insert_str_child(string_leaf) + string_line.append(string_leaf) + + old_rpar_leaf = None + if is_valid_index(string_idx + 1): + right_leaves = LL[string_idx + 1 :] + if ends_with_comma: + right_leaves.pop() + + if old_parens_exist: + assert ( + right_leaves and right_leaves[-1].type == token.RPAR + ), "Apparently, old parentheses do NOT exist?!" + old_rpar_leaf = right_leaves.pop() + + append_leaves(string_line, line, right_leaves) + + yield Ok(string_line) + + # --- Last Line + last_line = line.clone() + last_line.bracket_tracker = first_line.bracket_tracker + + new_rpar_leaf = Leaf(token.RPAR, ")") + if old_rpar_leaf is not None: + replace_child(old_rpar_leaf, new_rpar_leaf) + else: + insert_str_child(new_rpar_leaf) + last_line.append(new_rpar_leaf) + + # If the target string ended with a comma, we place this comma to the + # right of the RPAR on the last line. + if ends_with_comma: + comma_leaf = Leaf(token.COMMA, ",") + replace_child(LL[comma_idx], comma_leaf) + last_line.append(comma_leaf) + + yield Ok(last_line) + + +class StringParser: + """ + A state machine that aids in parsing a string's "trailer", which can be + either non-existent, an old-style formatting sequence (e.g. `% varX` or `% + (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX, + varY)`). + + NOTE: A new StringParser object MUST be instantiated for each string + trailer we need to parse. + + Examples: + We shall assume that `line` equals the `Line` object that corresponds + to the following line of python code: + ``` + x = "Some {}.".format("String") + some_other_string + ``` + + Furthermore, we will assume that `string_idx` is some index such that: + ``` + assert line.leaves[string_idx].value == "Some {}." + ``` + + The following code snippet then holds: + ``` + string_parser = StringParser() + idx = string_parser.parse(line.leaves, string_idx) + assert line.leaves[idx].type == token.PLUS + ``` + """ + + DEFAULT_TOKEN = -1 + + # String Parser States + START = 1 + DOT = 2 + NAME = 3 + PERCENT = 4 + SINGLE_FMT_ARG = 5 + LPAR = 6 + RPAR = 7 + DONE = 8 + + # Lookup Table for Next State + _goto: Dict[Tuple[ParserState, NodeType], ParserState] = { + # A string trailer may start with '.' OR '%'. + (START, token.DOT): DOT, + (START, token.PERCENT): PERCENT, + (START, DEFAULT_TOKEN): DONE, + # A '.' MUST be followed by an attribute or method name. + (DOT, token.NAME): NAME, + # A method name MUST be followed by an '(', whereas an attribute name + # is the last symbol in the string trailer. + (NAME, token.LPAR): LPAR, + (NAME, DEFAULT_TOKEN): DONE, + # A '%' symbol can be followed by an '(' or a single argument (e.g. a + # string or variable name). + (PERCENT, token.LPAR): LPAR, + (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG, + # If a '%' symbol is followed by a single argument, that argument is + # the last leaf in the string trailer. + (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE, + # If present, a ')' symbol is the last symbol in a string trailer. + # (NOTE: LPARS and nested RPARS are not included in this lookup table, + # since they are treated as a special case by the parsing logic in this + # classes' implementation.) + (RPAR, DEFAULT_TOKEN): DONE, + } + + def __init__(self) -> None: + self._state = self.START + self._unmatched_lpars = 0 + + def parse(self, leaves: List[Leaf], string_idx: int) -> int: + """ + Pre-conditions: + * @leaves[@string_idx].type == token.STRING + + Returns: + The index directly after the last leaf which is apart of the string + trailer, if a "trailer" exists. + OR + @string_idx + 1, if no string "trailer" exists. + """ + assert leaves[string_idx].type == token.STRING + + idx = string_idx + 1 + while idx < len(leaves) and self._next_state(leaves[idx]): + idx += 1 + return idx + + 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]`). + * On the next call to this function, the leaf parameter passed in + MUST be the leaf directly following @leaf. + + Returns: + True iff @leaf is apart of the string's trailer. + """ + # We ignore empty LPAR or RPAR leaves. + if is_empty_par(leaf): + return True + + next_token = leaf.type + if next_token == token.LPAR: + self._unmatched_lpars += 1 + + current_state = self._state + + # The LPAR parser state is a special case. We will return True until we + # find the matching RPAR token. + if current_state == self.LPAR: + if next_token == token.RPAR: + self._unmatched_lpars -= 1 + if self._unmatched_lpars == 0: + self._state = self.RPAR + # Otherwise, we use a lookup table to determine the next state. + else: + # If the lookup table matches the current state to the next + # token, we use the lookup table. + if (current_state, next_token) in self._goto: + self._state = self._goto[current_state, next_token] + else: + # Otherwise, we check if a the current state was assigned a + # default. + if (current_state, self.DEFAULT_TOKEN) in self._goto: + self._state = self._goto[current_state, self.DEFAULT_TOKEN] + # If no default has been assigned, then this parser has a logic + # error. + else: + raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!") + + if self._state == self.DONE: + return False + + return True + + +def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]: + """ + Factory for a convenience function that is used to orphan @string_leaf + and then insert multiple new leaves into the same part of the node + structure that @string_leaf had originally occupied. + + Examples: + Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N = + string_leaf.parent`. Assume the node `N` has the following + original structure: + + Node( + expr_stmt, [ + Leaf(NAME, 'x'), + Leaf(EQUAL, '='), + Leaf(STRING, '"foo"'), + ] + ) + + We then run the code snippet shown below. + ``` + insert_str_child = insert_str_child_factory(string_leaf) + + lpar = Leaf(token.LPAR, '(') + insert_str_child(lpar) + + bar = Leaf(token.STRING, '"bar"') + insert_str_child(bar) + + rpar = Leaf(token.RPAR, ')') + insert_str_child(rpar) + ``` + + After which point, it follows that `string_leaf.parent is None` and + the node `N` now has the following structure: + + Node( + expr_stmt, [ + Leaf(NAME, 'x'), + Leaf(EQUAL, '='), + Leaf(LPAR, '('), + Leaf(STRING, '"bar"'), + Leaf(RPAR, ')'), + ] + ) + """ + string_parent = string_leaf.parent + string_child_idx = string_leaf.remove() + + def insert_str_child(child: LN) -> None: + nonlocal string_child_idx + + assert string_parent is not None + assert string_child_idx is not None + + string_parent.insert_child(string_child_idx, child) + string_child_idx += 1 + + return insert_str_child + + +def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]: + """ + Examples: + ``` + my_list = [1, 2, 3] + + is_valid_index = is_valid_index_factory(my_list) + + assert is_valid_index(0) + assert is_valid_index(2) + + assert not is_valid_index(3) + assert not is_valid_index(-1) + ``` + """ + + def is_valid_index(idx: int) -> bool: + """ + Returns: + True iff @idx is positive AND seq[@idx] does NOT raise an + IndexError. + """ + return 0 <= idx < len(seq) + + return is_valid_index diff --git a/tests/test_black.py b/tests/test_black.py index b8e526a953e..29a0731dd9d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -34,6 +34,10 @@ import black from black import Feature, TargetVersion +from black.cache import get_cache_file +from black.debug import DebugVisitor +from black.report import Report +import black.files from pathspec import PathSpec @@ -69,7 +73,7 @@ def cache_dir(exists: bool = True) -> Iterator[Path]: cache_dir = Path(workspace) if not exists: cache_dir = cache_dir / "new" - with patch("black.CACHE_DIR", cache_dir): + with patch("black.cache.CACHE_DIR", cache_dir): yield cache_dir @@ -582,7 +586,7 @@ def test_tab_comment_indentation(self) -> None: self.assertFormatEqual(contents_spc, fs(contents_tab)) def test_report_verbose(self) -> None: - report = black.Report(verbose=True) + report = Report(verbose=True) out_lines = [] err_lines = [] @@ -592,7 +596,7 @@ def out(msg: str, **kwargs: Any) -> None: def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 1) self.assertEqual(len(err_lines), 0) @@ -684,7 +688,7 @@ def err(msg: str, **kwargs: Any) -> None: ) def test_report_quiet(self) -> None: - report = black.Report(quiet=True) + report = Report(quiet=True) out_lines = [] err_lines = [] @@ -694,7 +698,7 @@ def out(msg: str, **kwargs: Any) -> None: def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 0) self.assertEqual(len(err_lines), 0) @@ -788,7 +792,7 @@ def out(msg: str, **kwargs: Any) -> None: def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 0) self.assertEqual(len(err_lines), 0) @@ -1005,8 +1009,8 @@ def out(msg: str, **kwargs: Any) -> None: def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): - black.DebugVisitor.show(source) + with patch("black.debug.out", out): + DebugVisitor.show(source) actual = "\n".join(out_lines) + "\n" log_name = "" if expected != actual: @@ -1054,7 +1058,7 @@ def out(msg: str, **kwargs: Any) -> None: def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): with self.assertRaises(AssertionError): self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]") @@ -1066,7 +1070,7 @@ def err(msg: str, **kwargs: Any) -> None: def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: - cache_file = black.get_cache_file(mode) + 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), {}) @@ -1120,7 +1124,7 @@ def test_no_cache_when_writeback_diff(self) -> None: "black.write_cache" ) as write_cache: self.invokeBlack([str(src), "--diff"]) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) write_cache.assert_not_called() read_cache.assert_not_called() @@ -1135,7 +1139,7 @@ def test_no_cache_when_writeback_color_diff(self) -> None: "black.write_cache" ) as write_cache: self.invokeBlack([str(src), "--diff", "--color"]) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) write_cache.assert_not_called() read_cache.assert_not_called() @@ -1173,7 +1177,7 @@ def test_no_cache_when_stdin(self) -> None: black.main, ["-"], input=BytesIO(b"print('hello')") ) self.assertEqual(result.exit_code, 0) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) def test_read_cache_no_cachefile(self) -> None: @@ -1960,7 +1964,10 @@ def test_find_project_root(self) -> None: self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) - @patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__) + @patch( + "black.files.find_user_pyproject_toml", + black.files.find_user_pyproject_toml.__wrapped__, + ) def test_find_user_pyproject_toml_linux(self) -> None: if system() == "Windows": return @@ -1970,7 +1977,7 @@ def test_find_user_pyproject_toml_linux(self) -> None: tmp_user_config = Path(workspace) / "black" with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}): self.assertEqual( - black.find_user_pyproject_toml(), tmp_user_config.resolve() + black.files.find_user_pyproject_toml(), tmp_user_config.resolve() ) # Test fallback for XDG_CONFIG_HOME @@ -1978,7 +1985,7 @@ def test_find_user_pyproject_toml_linux(self) -> None: os.environ.pop("XDG_CONFIG_HOME", None) fallback_user_config = Path("~/.config").expanduser() / "black" self.assertEqual( - black.find_user_pyproject_toml(), fallback_user_config.resolve() + black.files.find_user_pyproject_toml(), fallback_user_config.resolve() ) def test_find_user_pyproject_toml_windows(self) -> None: @@ -1986,7 +1993,9 @@ def test_find_user_pyproject_toml_windows(self) -> None: return user_config_path = Path.home() / ".black" - self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve()) + self.assertEqual( + black.files.find_user_pyproject_toml(), user_config_path.resolve() + ) def test_bpo_33660_workaround(self) -> None: if system() == "Windows": diff --git a/tests/test_format.py b/tests/test_format.py index 78f2b558a4f..e59d5218d2e 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -71,10 +71,27 @@ SOURCES = [ - "tests/test_black.py", - "tests/test_format.py", - "tests/test_blackd.py", "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", @@ -86,6 +103,13 @@ "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/test_primer.py", + "tests/optional.py", + "tests/util.py", + "tests/conftest.py", ] diff --git a/tests/util.py b/tests/util.py index 3670952ba8c..1e86a3f3d78 100644 --- a/tests/util.py +++ b/tests/util.py @@ -2,9 +2,12 @@ import unittest from pathlib import Path from typing import List, Tuple, Any -import black from functools import partial +import black +from black.output import out, err +from black.debug import DebugVisitor + THIS_DIR = Path(__file__).parent PROJECT_ROOT = THIS_DIR.parent EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" @@ -26,21 +29,21 @@ class BlackBaseTestCase(unittest.TestCase): def assertFormatEqual(self, expected: str, actual: str) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): - bdv: black.DebugVisitor[Any] - black.out("Expected tree:", fg="green") + bdv: DebugVisitor[Any] + out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) - bdv = black.DebugVisitor() + bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: - black.err(str(ve)) - black.out("Actual tree:", fg="red") + err(str(ve)) + out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) - bdv = black.DebugVisitor() + bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: - black.err(str(ve)) + err(str(ve)) self.assertMultiLineEqual(expected, actual) From 036bea4aa0e2b9b3fe50d0d49addc811cce61fa4 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 8 May 2021 05:34:25 -0400 Subject: [PATCH 231/680] Speed up tests even more (#2205) There's three optimizations in this commit: 1. Don't check if Black's output is stable or equivalant if no changes were made in the first place. It's not like passing the same code (for both source and actual) through black.assert_equivalent or black.assert_stable is useful. It's not a big deal for the smaller tests, but it eats a lot of time in tests/test_format.py since its test cases are big. This is also closer to how Black works IRL. 2. Use a smaller file for `test_root_logger_not_used_directly` since the logging it's checking happens during blib2to3's startup so the file doesn't really matter. 3. If we're checking a file is formatting (i.e. test_source_is_formatted) don't run Black over it again with `black.format_file_in_place`. `tests/test_format.py::TestSimpleFormat.check_file` is good enough. --- tests/test_black.py | 7 ++++--- tests/test_format.py | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index 29a0731dd9d..347eaf621fe 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -175,8 +175,9 @@ def test_piping(self) -> None: ) self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) - black.assert_equivalent(source, result.output) - black.assert_stable(source, result.output, DEFAULT_MODE) + if source != result.output: + black.assert_equivalent(source, result.output) + black.assert_stable(source, result.output, DEFAULT_MODE) def test_piping_diff(self) -> None: diff_header = re.compile( @@ -1904,7 +1905,7 @@ def fail(*args: Any, **kwargs: Any) -> None: critical=fail, log=fail, ): - ff(THIS_FILE) + ff(THIS_DIR / "util.py") def test_invalid_config_return_code(self) -> None: tmp_file = Path(black.dump_to_file()) diff --git a/tests/test_format.py b/tests/test_format.py index e59d5218d2e..5c78afe0ba6 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -7,7 +7,6 @@ from tests.util import ( BlackBaseTestCase, fs, - ff, DEFAULT_MODE, dump_to_stderr, read_data, @@ -135,11 +134,11 @@ def test_experimental_format(self, filename: str) -> None: def test_source_is_formatted(self, filename: str) -> None: path = THIS_DIR.parent / filename self.check_file(str(path), DEFAULT_MODE, data=False) - self.assertFalse(ff(path)) 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) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, mode) + if source != actual: + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode) From 62bfbd6a63dcac2f6f31eb014f69397c9eb967d2 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 8 May 2021 15:17:38 -0400 Subject: [PATCH 232/680] Reorganize docs v2 (GH-2174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I know I know, this is the second reorganization of the docs. I'm not saying the first one was bad or anything... but.. actually wait nah, *it was bad*. Anyway, welcome to probably my biggest commit. The main thing with this reorganization was to introduce nesting to the documentation! Having all of the docs be part of the main TOC was becoming too much. There wasn't much room to expand either. Finally, the old setup required a documentation generation step which was just annoying. The goals of this reorganization was to: 1. Significantly restructure the docs to be discoverable and understandable 2. Add room for further docs (like guides or contributing docs) 3. Get rid of the doc generation step (it was slow and frustrating) 4. Unblock other improvements and also just make contributing to the docs easier Another important change with this is that we are no longer using GitHub as a documentation host. While GitHub does support Markdown based docs actually pretty well, the lack of any features outside of GitHub Flavoured Markdown is quite limiting. ReadTheDocs is just much better suited for documentation. You can use reST, MyST, CommonMark, and all of their great features like toctrees and admonitions. Related to this change, we're adopting MyST as our flavour of Markdown. MyST introduces neat syntax extensions to Markdown that pretty much gives us the best of both worlds. The ease of use and simplicity of MD and the flexibility and expressiveness of reST. Also recommonmark is deprecated now. This switch was possible now we don't use GH as a docs host. MyST docs have to be built to really be usable / pretty, so the MD docs are going to look pretty bad on GH, but that's fine now! Another thing that should be noted is that the README has been stripped of most content since it was confusing. Users would read the README and then think some feature or bug was fixed already and is available in a release when in reality, they weren't. They were reading effectively the latest docs without knowing. See also: https://github.com/psf/black/issues/1759 FYI: CommonMark is a rationalized version of Markdown syntax -- Commit history before merge: * Switch to MyST-Parser + doc config cleanup recommonmark is being deprecated in favour of MyST-Parser. This change is welcomed, especially since MyST-Parser has syntax extensions for the Commonmark standard. Effectively we get to use a language that's powerful and expressive like ReST, but get the simplicity of Markdown. The rest of this effort will be using some MyST features. This reorganization efforts aims to remove as much duplication as possible. The regeneration step once needed is gone, significantly simplifing our Sphinx documentation configuration. * Tell pipenv we replaced recommonmark for MyST-Parser Also update `docs/requirements.txt` * Delete all auto generated content * Switch prettier for mdformat (plus a few plugins) **FYI: THIS WAS EFFECTIVELY REVERTED, SEE THIRD TO LAST COMMIT** prettier doesn't support MyST's syntax extensions which are going to be used in this reorganization effort so we have to switch formatter. Unfortanately mdformat's style is different from prettier's so time to reformat the whole repo too. We're excluding .github/ISSUE_TEMPLATE because I have no idea whether its changes are safe, so let's play it safe. * Fix the heading levels in CHANGES.md + a link MyST-Parser / sphinx's linkcheck complains otherwise. * Move reference docs into a docs/contributing dir They're for contributors of Black anyway. Also added a note in the summary document warning about the lack of attention the reference has been dealing with. * Rewrite and setup the new landing page + main TOC - add some more detail about Black's beta status - add licensing info - add external links in the main TOC for GitHub, PyPI, and IRC - prepare main TOC for new structure * Break out AUTHORS into its own file Not only was the AUTHORS list quite long, this makes it easy to include it in the Sphinx docs with just a simple symlink. * Add license to docs via a simple include Yes the document is orphaned but it is linked to in the landing page (docs/index.rst). * Add "The Black Code Style" section This mostly was a restructuring commit, there has been a few updates but not many. The main goal was to split "current style" and "planned changes to the style that haven't happened yet" to avoid confusion. * Add "Getting Started" page This is basically a quick start + even more. This commit is certainly one of most creatively involved in this effort. * Add "Usage and Configuration" section This commit was as much restructuring as new content. Instead of being in one giant file, usage and configuration documentation can expand without bloating a single file. * Add "Integrations" section Just a restructuring commit ... * Add "Guides" section This is a promising area of documentation that could easily grow in the future, let's prepare for that! * Add "Contributing" section This is also another area that I expect to see significant growth in. Contributors to Black could definitely do with some more specific docs that clears up certain parts of our slightly confusing project (it's only confusing because we're getting big and old!). * Rewrite CONTRIBUTING.md to just point to RTD * Rewrite README.md to delegate most info to RTD * Address feedback + a lot of corrections and edits I know I said I wanted to do these after landing this but given there's going to be no time between this being merged and a release getting pushed, I want these changes to make it in. - drop the number flag for mdformat - to reduce diffs, see also: https://mdformat.readthedocs.io/en/stable/users/style.html#ordered-lists - the GH issue templates should be safe by mdformat, so get rid of the exclude - clarify our configuration position - i.e. stop claiming we don't have many options, instead say we want as little formatting knobs as possible - lots and lots of punctuation, spelling, and grammar corrections (thanks Jelle!) - use RTD as the source for the CHANGELOG too - visual style cleanups - add docs about our .gitignore behaviour - expand GHA Action docs - clarify we want the PR number in the CHANGELOG entry - claify Black's behaviour for with statements post Python 3.9 - italicize a bunch of "Black"s Thank you goes to Jelle, Taneli (hukkinj1 on GH), Felix (felix-hilden on GH), and Wouter (wbolster on GH) for the feedback! * Merge remote-tracking branch 'upstream/master' into reorganize-docs-v2 merge conflicts suck, although these ones weren't too bad. * Add changelog entry + fix merge conflict resolution error I consider this important enough to be worthy of a changelog entry :) * Merge branch 'master' into reorganize-docs-v2 Co-authored-by: Łukasz Langa * Actually let's continue using prettier Prettier works fine for all of the default MyST syntax so let's not rock the boat as much. Dropping the mdformat commit was merge-conflict filled so here's additional commit instead. * Address Cooper's, Taneli's, and Jelle's feedback Lots of wording improvements by Cooper. Taneli suggested to disable the enabled by default MyST syntax not supported by Prettier and I agreed. And Jelle found one more spelling error! * More minor fixes --- .github/ISSUE_TEMPLATE/bug_report.md | 14 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- AUTHORS.md | 184 ++++++ CHANGES.md | 94 +-- CONTRIBUTING.md | 114 +--- Pipfile | 2 +- Pipfile.lock | 131 +++- README.md | 621 ++---------------- docs/authors.md | 188 +----- docs/black_primer.md | 120 ---- docs/conf.py | 229 +------ docs/contributing/gauging_changes.md | 61 ++ docs/contributing/index.rst | 39 ++ .../reference/reference_classes.rst | 0 .../reference/reference_exceptions.rst | 0 .../reference/reference_functions.rst | 0 .../reference/reference_summary.rst | 5 + .../the_basics.md} | 35 +- docs/getting_started.md | 49 ++ docs/github_actions.md | 27 - docs/guides/index.rst | 14 + .../introducing_black_to_your_project.md | 50 ++ .../using_black_with_other_tools.md} | 36 +- docs/index.rst | 85 ++- docs/installation_and_usage.md | 195 ------ .../editors.md} | 52 +- docs/integrations/github_actions.md | 35 + docs/integrations/index.rst | 28 + docs/integrations/source_version_control.md | 14 + docs/license.rst | 6 + docs/pyproject_toml.md | 91 --- docs/requirements.txt | 5 +- docs/show_your_style.md | 19 - .../current_style.md} | 59 +- docs/the_black_code_style/future_style.md | 35 + docs/the_black_code_style/index.rst | 19 + .../black_as_a_server.md} | 6 +- .../file_collection_and_discovery.md} | 18 +- docs/usage_and_configuration/index.rst | 24 + docs/usage_and_configuration/the_basics.md | 227 +++++++ docs/version_control_integration.md | 28 - 41 files changed, 1200 insertions(+), 1761 deletions(-) create mode 100644 AUTHORS.md mode change 100644 => 120000 docs/authors.md delete mode 100644 docs/black_primer.md create mode 100644 docs/contributing/gauging_changes.md create mode 100644 docs/contributing/index.rst rename docs/{ => contributing}/reference/reference_classes.rst (100%) rename docs/{ => contributing}/reference/reference_exceptions.rst (100%) rename docs/{ => contributing}/reference/reference_functions.rst (100%) rename docs/{ => contributing}/reference/reference_summary.rst (51%) rename docs/{contributing_to_black.md => contributing/the_basics.md} (68%) create mode 100644 docs/getting_started.md delete mode 100644 docs/github_actions.md create mode 100644 docs/guides/index.rst create mode 100644 docs/guides/introducing_black_to_your_project.md rename docs/{compatible_configs.md => guides/using_black_with_other_tools.md} (92%) delete mode 100644 docs/installation_and_usage.md rename docs/{editor_integration.md => integrations/editors.md} (85%) create mode 100644 docs/integrations/github_actions.md create mode 100644 docs/integrations/index.rst create mode 100644 docs/integrations/source_version_control.md create mode 100644 docs/license.rst delete mode 100644 docs/pyproject_toml.md delete mode 100644 docs/show_your_style.md rename docs/{the_black_code_style.md => the_black_code_style/current_style.md} (91%) create mode 100644 docs/the_black_code_style/future_style.md create mode 100644 docs/the_black_code_style/index.rst rename docs/{blackd.md => usage_and_configuration/black_as_a_server.md} (98%) rename docs/{ignoring_unmodified_files.md => usage_and_configuration/file_collection_and_discovery.md} (61%) create mode 100644 docs/usage_and_configuration/index.rst create mode 100644 docs/usage_and_configuration/the_basics.md delete mode 100644 docs/version_control_integration.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f4a438622aa..3b59906594a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,21 +11,21 @@ assignees: "" **To Reproduce** Steps to reproduce the behavior: 1. Take this file '...' -2. Run _Black_ on it with these arguments '....' -3. See error +1. Run _Black_ on it with these arguments '....' +1. See error **Expected behavior** A clear and concise description of what you expected to happen. **Environment (please complete the following information):** -- Version: [e.g. master] -- OS and Python version: [e.g. Linux/Python 3.7.4rc1] +- Version: \[e.g. master\] +- OS and Python version: \[e.g. Linux/Python 3.7.4rc1\] **Does this bug also happen on master?** To answer this, you have two options: -1. Use the online formatter at https://black.now.sh/?version=master, which will use the - latest master branch. -2. Or run _Black_ on your machine: +1. Use the online formatter at , which will + use the latest master branch. +1. 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]`; diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 00dd5dd8fe5..56c2f0d0185 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,7 +7,7 @@ assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise -description of what the problem is. Ex. I'm always frustrated when [...] +description of what the problem is. Ex. I'm always frustrated when \[...\] **Describe the solution you'd like** A clear and concise description of what you want to happen. diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000000..cb79dec3de3 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,184 @@ +# Authors + +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. + +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) +- [Alex Vandiver](mailto:github@chmrr.net) +- [Allan Simon](mailto:allan.simon@supinfo.com) +- Anders-Petter Ljungquist +- [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) +- [Andrew Zhou](mailto:andrewfzhou@gmail.com) +- [Andrey](mailto:dyuuus@yandex.ru) +- [Andy Freeland](mailto:andy@andyfreeland.net) +- [Anthony Sottile](mailto:asottile@umich.edu) +- [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) +- [Arnav Borbornah](mailto:arnavborborah11@gmail.com) +- [Artem Malyshev](mailto:proofit404@gmail.com) +- [Asger Hautop Drewsen](mailto:asgerdrewsen@gmail.com) +- [Augie Fackler](mailto:raf@durin42.com) +- [Aviskar KC](mailto:aviskarkc10@gmail.com) +- Batuhan Taşkaya +- [Benjamin Wohlwend](mailto:bw@piquadrat.ch) +- [Benjamin Woodruff](mailto:github@benjam.info) +- [Bharat Raghunathan](mailto:bharatraghunthan9767@gmail.com) +- [Brandt Bucher](mailto:brandtbucher@gmail.com) +- [Brett Cannon](mailto:brett@python.org) +- [Bryan Bugyi](mailto:bryan.bugyi@rutgers.edu) +- [Bryan Forbes](mailto:bryan@reigndropsfall.net) +- [Calum Lind](mailto:calumlind@gmail.com) +- [Charles](mailto:peacech@gmail.com) +- Charles Reid +- [Christian Clauss](mailto:cclauss@bluewin.ch) +- [Christian Heimes](mailto:christian@python.org) +- [Chuck Wooters](mailto:chuck.wooters@microsoft.com) +- [Chris Rose](mailto:offline@offby1.net) +- Codey Oxley +- [Cong](mailto:congusbongus@gmail.com) +- [Cooper Ry Lees](mailto:me@cooperlees.com) +- [Dan Davison](mailto:dandavison7@gmail.com) +- [Daniel Hahler](mailto:github@thequod.de) +- [Daniel M. Capella](mailto:polycitizen@gmail.com) +- Daniele Esposti +- [David Hotham](mailto:david.hotham@metaswitch.com) +- [David Lukes](mailto:dafydd.lukes@gmail.com) +- [David Szotten](mailto:davidszotten@gmail.com) +- [Denis Laxalde](mailto:denis@laxalde.org) +- [Douglas Thor](mailto:dthor@transphormusa.com) +- dylanjblack +- [Eli Treuherz](mailto:eli@treuherz.com) +- [Emil Hessman](mailto:emil@hessman.se) +- [Felix Kohlgrüber](mailto:felix.kohlgrueber@gmail.com) +- [Florent Thiery](mailto:fthiery@gmail.com) +- Francisco +- [Giacomo Tagliabue](mailto:giacomo.tag@gmail.com) +- [Greg Gandenberger](mailto:ggandenberger@shoprunner.com) +- [Gregory P. Smith](mailto:greg@krypto.org) +- Gustavo Camargo +- hauntsaninja +- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) +- [Heaford](mailto:dan@heaford.com) +- [Hugo Barrera](mailto::hugo@barrera.io) +- Hugo van Kemenade +- [Hynek Schlawack](mailto:hs@ox.cx) +- [Ivan Katanić](mailto:ivan.katanic@gmail.com) +- [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) +- [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) +- [Jan Hnátek](mailto:jan.hnatek@gmail.com) +- [Jason Fried](mailto:me@jasonfried.info) +- [Jason Friedland](mailto:jason@friedland.id.au) +- [jgirardet](mailto:ijkl@netc.fr) +- Jim Brännlund +- [Jimmy Jia](mailto:tesrin@gmail.com) +- [Joe Antonakakis](mailto:jma353@cornell.edu) +- [Jon Dufresne](mailto:jon.dufresne@gmail.com) +- [Jonas Obrist](mailto:ojiidotch@gmail.com) +- [Jonty Wareing](mailto:jonty@jonty.co.uk) +- [Jose Nazario](mailto:jose.monkey.org@gmail.com) +- [Joseph Larson](mailto:larson.joseph@gmail.com) +- [Josh Bode](mailto:joshbode@fastmail.com) +- [Josh Holland](mailto:anowlcalledjosh@gmail.com) +- [Joshua Cannon](mailto:joshdcannon@gmail.com) +- [José Padilla](mailto:jpadilla@webapplicate.com) +- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) +- [kaiix](mailto:kvn.hou@gmail.com) +- [Katie McLaughlin](mailto:katie@glasnt.com) +- Katrin Leinweber +- [Keith Smiley](mailto:keithbsmiley@gmail.com) +- [Kenyon Ralph](mailto:kenyon@kenyonralph.com) +- [Kevin Kirsche](mailto:Kev.Kirsche+GitHub@gmail.com) +- [Kyle Hausmann](mailto:kyle.hausmann@gmail.com) +- [Kyle Sunden](mailto:sunden@wisc.edu) +- Lawrence Chan +- [Linus Groh](mailto:mail@linusgroh.de) +- [Loren Carvalho](mailto:comradeloren@gmail.com) +- [Luka Sterbic](mailto:luka.sterbic@gmail.com) +- [LukasDrude](mailto:mail@lukas-drude.de) +- Mahmoud Hossam +- Mariatta +- [Matt VanEseltine](mailto:vaneseltine@gmail.com) +- [Matthew Clapp](mailto:itsayellow+dev@gmail.com) +- [Matthew Walster](mailto:matthew@walster.org) +- Max Smolens +- [Michael Aquilina](mailto:michaelaquilina@gmail.com) +- [Michael Flaxman](mailto:michael.flaxman@gmail.com) +- [Michael J. Sullivan](mailto:sully@msully.net) +- [Michael McClimon](mailto:michael@mcclimon.org) +- [Miguel Gaiowski](mailto:miggaiowski@gmail.com) +- [Mike](mailto:roshi@fedoraproject.org) +- [mikehoyio](mailto:mikehoy@gmail.com) +- [Min ho Kim](mailto:minho42@gmail.com) +- [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) +- MomIsBestFriend +- [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) +- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) +- [Neraste](mailto:neraste.herr10@gmail.com) +- [Nikolaus Waxweiler](mailto:madigens@gmail.com) +- [Ofek Lev](mailto:ofekmeister@gmail.com) +- [Osaetin Daniel](mailto:osaetindaniel@gmail.com) +- [otstrel](mailto:otstrel@gmail.com) +- [Pablo Galindo](mailto:Pablogsal@gmail.com) +- [Paul Ganssle](mailto:p.ganssle@gmail.com) +- [Paul Meinhardt](mailto:mnhrdt@gmail.com) +- [Peter Bengtsson](mailto:mail@peterbe.com) +- [Peter Grayson](mailto:pete@jpgrayson.net) +- [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) +- pmacosta +- [Quentin Pradet](mailto:quentin@pradet.me) +- [Ralf Schmitt](mailto:ralf@systemexit.de) +- [Ramón Valles](mailto:mroutis@protonmail.com) +- [Richard Fearn](mailto:richardfearn@gmail.com) +- [Rishikesh Jha](mailto:rishijha424@gmail.com) +- [Rupert Bedford](mailto:rupert@rupertb.com) +- Russell Davis +- [Rémi Verschelde](mailto:rverschelde@gmail.com) +- [Sami Salonen](mailto:sakki@iki.fi) +- [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) +- [Sanket Dasgupta](mailto:sanketdasgupta@gmail.com) +- Sergi +- [Scott Stevenson](mailto:scott@stevenson.io) +- Shantanu +- [shaoran](mailto:shaoran@sakuranohana.org) +- [Shinya Fujino](mailto:shf0811@gmail.com) +- springstan +- [Stavros Korokithakis](mailto:hi@stavros.io) +- [Stephen Rosen](mailto:sirosen@globus.org) +- [Steven M. Vascellaro](mailto:S.Vascellaro@gmail.com) +- [Sunil Kapil](mailto:snlkapil@gmail.com) +- [Sébastien Eustace](mailto:sebastien.eustace@gmail.com) +- [Tal Amuyal](mailto:TalAmuyal@gmail.com) +- [Terrance](mailto:git@terrance.allofti.me) +- [Thom Lu](mailto:thomas.c.lu@gmail.com) +- [Thomas Grainger](mailto:tagrain@gmail.com) +- [Tim Gates](mailto:tim.gates@iress.com) +- [Tim Swast](mailto:swast@google.com) +- [Timo](mailto:timo_tk@hotmail.com) +- Toby Fleming +- [Tom Christie](mailto:tom@tomchristie.com) +- [Tony Narlock](mailto:tony@git-pull.com) +- [Tsuyoshi Hombashi](mailto:tsuyoshi.hombashi@gmail.com) +- [Tushar Chandra](mailto:tusharchandra2018@u.northwestern.edu) +- [Tzu-ping Chung](mailto:uranusjr@gmail.com) +- [Utsav Shah](mailto:ukshah2@illinois.edu) +- utsav-dbx +- vezeli +- [Ville Skyttä](mailto:ville.skytta@iki.fi) +- [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com) +- [Vlad Emelianov](mailto:volshebnyi@gmail.com) +- [williamfzc](mailto:178894043@qq.com) +- [wouter bolsterlee](mailto:wouter@bolsterl.ee) +- Yazdan +- [Yngve Høiseth](mailto:yngve@hoiseth.net) +- [Yurii Karabas](mailto:1998uriyyo@gmail.com) +- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/CHANGES.md b/CHANGES.md index 8cbdeca44f7..9b155924b7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,26 +1,32 @@ -## Change Log +# Change Log -### Unreleased +## Unreleased -#### _Black_ +### _Black_ - Refactor `src/black/__init__.py` into many files (#2206) -### 21.5b0 +### Documentation -#### _Black_ +- Sigificantly 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) + +## 21.5b0 + +### _Black_ - Set `--pyi` mode if `--stdin-filename` ends in `.pyi` (#2169) - Stop detecting target version as Python 3.9+ with pre-PEP-614 decorators that are being called but with no arguments (#2182) -#### _Black-Primer_ +### _Black-Primer_ - Add `--no-diff` to black-primer to suppress formatting changes (#2187) -### 21.4b2 +## 21.4b2 -#### _Black_ +### _Black_ - Fix crash if the user configuration directory is inaccessible. (#2158) @@ -31,15 +37,15 @@ - Allow `.gitignore` rules to be overridden by specifying `exclude` in `pyproject.toml` or on the command line. (#2170) -#### _Packaging_ +### _Packaging_ - Install `primer.json` (used by `black-primer` by default) with black. (#2154) -### 21.4b1 +## 21.4b1 -#### _Black_ +### _Black_ -- Fix crash on docstrings ending with "\ ". (#2142) +- Fix crash on docstrings ending with "\\ ". (#2142) - Fix crash when atypical whitespace is cleaned out of dostrings (#2120) @@ -50,13 +56,13 @@ - Don't remove necessary parentheses from assignment expression containing assert / return statements. (#2143) -#### _Packaging_ +### _Packaging_ - Bump pathspec to >= 0.8.1 to solve invalid .gitignore exclusion handling -### 21.4b0 +## 21.4b0 -#### _Black_ +### _Black_ - Fixed a rare but annoying formatting instability created by the combination of optional trailing commas inserted by `Black` and optional parentheses looking at @@ -112,21 +118,21 @@ - Fixed "Black produced code that is not equivalent to the source" when formatting Python 2 docstrings (#2037) -#### _Packaging_ +### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub Releases (#1743) -### 20.8b1 +## 20.8b1 -#### _Packaging_ +### _Packaging_ - explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions older than 7.0 -### 20.8b0 +## 20.8b0 -#### _Black_ +### _Black_ - re-implemented support for explicit trailing commas: now it works consistently within any bracket pair, including nested structures (#1288 and duplicates) @@ -170,11 +176,11 @@ this is an undocumented and unsupported feature, you lose Internet points for depending on it (#1609) -#### Vim plugin +### Vim plugin - prefer virtualenv packages over global packages (#1383) -### 19.10b0 +## 19.10b0 - added support for PEP 572 assignment expressions (#711) @@ -233,7 +239,7 @@ - `blackd` can now output the diff of formats on source code when the `X-Diff` header is provided (#969) -### 19.3b0 +## 19.3b0 - new option `--target-version` to control which Python versions _Black_-formatted code should target (#618) @@ -258,7 +264,7 @@ - `blackd` now supports CORS (#622) -### 18.9b0 +## 18.9b0 - numeric literals are now formatted by _Black_ (#452, #461, #464, #469): @@ -273,7 +279,9 @@ - hexadecimal digits are always uppercased (e.g. `0xBADC0DE`) -- added `blackd`, see [its documentation](#blackd) for more info (#349) +- added `blackd`, see + [its documentation](https://github.com/psf/black/blob/18.9b0/README.md#blackd) for + more info (#349) - adjacent string literals are now correctly split into multiple lines (#463) @@ -300,11 +308,11 @@ - note: the Vim plugin stopped registering `,=` as a default chord as it turned out to be a bad idea (#415) -### 18.6b4 +## 18.6b4 - hotfix: don't freeze when multiple comments directly precede `# fmt: off` (#371) -### 18.6b3 +## 18.6b3 - typing stub files (`.pyi`) now have blank lines added after constants (#340) @@ -332,7 +340,7 @@ - fixed a crash due to symbolic links pointing outside of the project directory (#338) -### 18.6b2 +## 18.6b2 - added `--config` (#65) @@ -346,13 +354,13 @@ - fixed unnecessary slowdown in comment placement calculation on lines without comments -### 18.6b1 +## 18.6b1 - hotfix: don't output human-facing information on stdout (#299) - hotfix: don't output cake emoji on non-zero return code (#300) -### 18.6b0 +## 18.6b0 - added `--include` and `--exclude` (#270) @@ -370,7 +378,7 @@ - _Black_ now preserves line endings when formatting a file in place (#258) -### 18.5b1 +## 18.5b1 - added `--pyi` (#249) @@ -399,7 +407,7 @@ - fixed extra empty line between a function signature and an inner function or inner class (#196) -### 18.5b0 +## 18.5b0 - call chains are now formatted according to the [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67) @@ -454,11 +462,11 @@ - fixed crash when dead symlinks where encountered -### 18.4a4 +## 18.4a4 - don't populate the cache on `--check` (#175) -### 18.4a3 +## 18.4a3 - added a "cache"; files already reformatted that haven't changed on disk won't be reformatted again (#109) @@ -486,7 +494,7 @@ - fixed missing splits of ternary expressions (#141) -### 18.4a2 +## 18.4a2 - fixed parsing of unaligned standalone comments (#99, #112) @@ -497,7 +505,7 @@ - fixed unstable formatting when encountering unnecessarily escaped quotes in a string (#120) -### 18.4a1 +## 18.4a1 - added `--quiet` (#78) @@ -509,7 +517,7 @@ - fixed removing backslash escapes from raw strings (#100, #105) -### 18.4a0 +## 18.4a0 - added `--diff` (#87) @@ -534,7 +542,7 @@ - only allow up to two empty lines on module level and only single empty lines within functions (#74) -### 18.3a4 +## 18.3a4 - `# fmt: off` and `# fmt: on` are implemented (#5) @@ -556,7 +564,7 @@ [Sphinx auto-attribute comments](http://www.sphinx-doc.org/en/stable/ext/autodoc.html#directive-autoattribute) (#68) -### 18.3a3 +## 18.3a3 - don't remove single empty lines outside of bracketed expressions (#19) @@ -566,7 +574,7 @@ - even better handling of numpy-style array indexing (#33, again) -### 18.3a2 +## 18.3a2 - changed positioning of binary operators to occur at beginning of lines instead of at the end, following @@ -590,7 +598,7 @@ - fixed spurious space after star-based unary expressions (#31) -### 18.3a1 +## 18.3a1 - added `--check` @@ -609,10 +617,10 @@ - fixed spurious space after unary operators when the operand was a complex expression (#15) -### 18.3a0 +## 18.3a0 - first published version, Happy 🍰 Day 2018! - alpha quality -- date-versioned (see: https://calver.org/) +- date-versioned (see: ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2a4ee20c99..10f60422f04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,114 +3,8 @@ 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? -## Bird's eye view +Our [contributing documentation](https://black.readthedocs.org/en/latest/contributing/) +contains details on all you need to know about contributing to _Black_, the basics to +the internals of _Black_. -In terms of inspiration, _Black_ is about as configurable as _gofmt_. This is -deliberate. - -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. - -## Technicalities - -Development on the latest version of Python is preferred. As of this writing it's 3.9. -You can use any operating system. I am using macOS myself and CentOS at work. - -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 -$ pip install -e .[d] -``` - -Before submitting pull requests, run lints and tests with the following commands from -the root of the black repo: - -```console -# Linting -$ pre-commit run -a - -# Unit tests -$ tox -e py - -# Optional Fuzz testing -$ tox -e fuzz - -# Optional CI run to test your changes on many popular python projects -$ black-primer [-k -w /tmp/black_test_repos] -``` - -### News / Changelog Requirement - -`Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If -you feel this PR not require a changelog entry please state that in a comment and a -maintainer can add a `skip news` label to make the CI pass. Otherwise, please ensure you -have a line in the following format: - -```md -- `Black` is now more awesome (#X) -``` - -To workout X, please use -[Next PR Number](https://ichard26.github.io/next-pr-number/?owner=psf&name=black). This -is not perfect but saves a lot of release overhead as now the releaser does not need to -go back and workout what to add to the `CHANGES.md` for each release. - -### Style Changes - -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. - -### Docs Testing - -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/ -``` - -## 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/master/src/black_primer/primer.json) -configuration for it to pass. - -For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). - -## Hygiene - -If you're fixing a bug, add a test. Run it first to confirm it fails, then fix the bug, -run it again to confirm it's really fixed. - -If adding a new feature, add a test. In fact, always add a test. But wait, before adding -any large feature, first open an issue for us to discuss the idea first. - -## Finally - -Thanks again for your interest in improving the project! You're taking action when most -people decide to sit and watch. +We look forward to your contributions! diff --git a/Pipfile b/Pipfile index 68562f5e53f..d1842ff749b 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ flake8-bugbear = "*" mypy = ">=0.812" pre-commit = "*" readme_renderer = "*" -recommonmark = "*" +MyST-Parser = ">=0.13.7" setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" diff --git a/Pipfile.lock b/Pipfile.lock index 8a16a75026a..f973c8423e8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1b075e0e344dad54944ed4474351c3f311accb67cbea2e0c381da3700b71f415" + "sha256": "62bc4bdb0117234d1f374b2dc0685369f6df7c7192d16409cc9c42a429770166" }, "pipfile-spec": 6, "requires": {}, @@ -428,11 +428,11 @@ }, "babel": { "hashes": [ - "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", - "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" + "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.0" + "version": "==2.9.1" }, "black": { "editable": true, @@ -456,6 +456,48 @@ ], "version": "==2020.12.5" }, + "cffi": { + "hashes": [ + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" + }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -488,13 +530,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, "coverage": { "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", @@ -553,6 +588,24 @@ "index": "pypi", "version": "==5.5" }, + "cryptography": { + "hashes": [ + "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", + "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", + "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", + "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", + "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", + "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", + "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", + "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", + "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", + "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", + "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", + "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.7" + }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -624,6 +677,14 @@ "markers": "python_version >= '3.6'", "version": "==4.0.1" }, + "jeepney": { + "hashes": [ + "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", + "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.6.0" + }, "jinja2": { "hashes": [ "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", @@ -640,6 +701,14 @@ "markers": "python_version >= '3.6'", "version": "==23.0.1" }, + "markdown-it-py": { + "hashes": [ + "sha256:30b3e9f8198dc82a5df0dcb73fd31d56cd9a43bf8a747feb10b2ba74f962bcb1", + "sha256:c3b9f995be0792cbbc8ab2f53d74072eb7ff8a8b622be8d61d38ab879709eca3" + ], + "markers": "python_version ~= '3.6'", + "version": "==0.6.2" + }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -705,6 +774,14 @@ ], "version": "==0.6.1" }, + "mdit-py-plugins": { + "hashes": [ + "sha256:1e467ca2ea056e8065cbd5d6c61e5052bb50826bde84c40f6a5ed77e82125710", + "sha256:77fd75dad81109ee91f30eb49146196f79afbbae041f298ae4886c8c2b5e23d7" + ], + "markers": "python_version ~= '3.6'", + "version": "==0.2.6" + }, "multidict": { "hashes": [ "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", @@ -784,6 +861,14 @@ "index": "pypi", "version": "==0.4.3" }, + "myst-parser": { + "hashes": [ + "sha256:260355b4da8e8865fe080b0638d7f1ab1791dc4bed02a7a48630b6bad4249219", + "sha256:e4bc99e43e19f70d22e528de8e7cce59f7e8e7c4c34dcba203de92de7a7c7c85" + ], + "index": "pypi", + "version": "==0.13.7" + }, "nodeenv": { "hashes": [ "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", @@ -830,6 +915,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", @@ -904,14 +997,6 @@ "index": "pypi", "version": "==29.0" }, - "recommonmark": { - "hashes": [ - "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f", - "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67" - ], - "index": "pypi", - "version": "==0.7.1" - }, "regex": { "hashes": [ "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", @@ -981,6 +1066,14 @@ ], "version": "==1.4.0" }, + "secretstorage": { + "hashes": [ + "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", + "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.1" + }, "setuptools-scm": { "hashes": [ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", diff --git a/README.md b/README.md index 6443569d0d0..ceccbb79962 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,12 @@ becomes transparent after a while and you can focus on the content instead. _Black_ makes code review faster by producing the smallest diffs possible. -Try it out now using the [Black Playground](https://black.now.sh). Watch the +Try it out now using the [Black Playground](https://black.vercel.app). Watch the [PyCon 2019 talk](https://youtu.be/esZLCuWs_2Y) to learn more. --- -_Contents:_ **[Installation and usage](#installation-and-usage)** | -**[Code style](#the-black-code-style)** | **[Pragmatism](#pragmatism)** | -**[pyproject.toml](#pyprojecttoml)** | **[Editor integration](#editor-integration)** | -**[blackd](#blackd)** | **[black-primer](#black-primer)** | -**[Version control integration](#version-control-integration)** | -**[GitHub Actions](#github-actions)** | -**[Ignoring unmodified files](#ignoring-unmodified-files)** | **[Used by](#used-by)** | -**[Testimonials](#testimonials)** | **[Show your style](#show-your-style)** | -**[Contributing](#contributing-to-black)** | **[Change log](#change-log)** | -**[Authors](#authors)** +**[Read the documentation on ReadTheDocs!](https://black.readthedocs.io/en/stable)** --- @@ -52,8 +43,6 @@ _Black_ can be installed by running `pip install black`. It requires Python 3.6. run. If you want to format Python 2 code as well, install with `pip install black[python2]`. -#### Install from GitHub - If you can't wait for the latest _hotness_ and want to install from GitHub, use: `pip install git+git://github.com/psf/black` @@ -72,392 +61,72 @@ You can run _Black_ as a package if running it as a script doesn't work: python -m black {source_file_or_directory} ``` -### Command line options - -_Black_ doesn't provide many options. You can list them by running `black --help`: - -```text -Usage: black [OPTIONS] [SRC]... - - The uncompromising code formatter. - -Options: - -c, --code TEXT Format the code passed in as a string. - -l, --line-length INTEGER How many characters per line to allow. - [default: 88] - - -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] - Python versions that should be supported by - Black's output. [default: per-file auto- - detection] - - --pyi Format all input files like typing stubs - regardless of file extension (useful when - piping source on standard input). - - -S, --skip-string-normalization - Don't normalize string quotes or prefixes. - -C, --skip-magic-trailing-comma - Don't use trailing commas as a reason to - split lines. - - --check Don't write the files back, just return the - status. Return code 0 means nothing would - change. Return code 1 means some files - would be reformatted. Return code 123 means - there was an internal error. - - --diff Don't write the files back, just output a - diff for each file on stdout. - - --color / --no-color Show colored diff. Only applies when - `--diff` is given. - - --fast / --safe If --fast given, skip temporary sanity - checks. [default: --safe] - - --include TEXT 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. - [default: \.pyi?$] - - --exclude TEXT 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: /(\.direnv|\.eggs|\.git|\. - hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.sv - n|_build|buck-out|build|dist)/] - - --extend-exclude TEXT 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 TEXT Like --exclude, but files and directories - matching this regex will be excluded even - when they are passed explicitly as - arguments. - - - --stdin-filename TEXT 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. - - -q, --quiet Don't emit non-error messages to stderr. - Errors are still emitted; silence those with - 2>/dev/null. - - -v, --verbose Also emit messages to stderr about files - that were not changed or were ignored due to - exclusion patterns. - - --version Show the version and exit. - --config FILE Read configuration from FILE path. - -h, --help Show this message and exit. -``` - -_Black_ is a well-behaved Unix-style command-line tool: - -- it does nothing if no sources are passed to it; -- 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). - -### Using _Black_ with other tools - -While _Black_ enforces formatting that conforms to PEP 8, other tools may raise warnings -about _Black_'s changes or will overwrite _Black_'s changes. A good example of this is -[isort](https://pypi.org/p/isort). Since _Black_ is barely configurable, these tools -should be configured to neither warn about nor overwrite _Black_'s changes. - -Actual details on _Black_ compatible configurations for various tools can be found in -[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#black-compatible-configurations). - -### Migrating your code style without ruining git blame - -A long-standing argument against moving to automated code formatters like _Black_ is -that the migration will clutter up the output of `git blame`. This was a valid argument, -but since Git version 2.23, Git natively supports -[ignoring revisions in blame](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt) -with the `--ignore-rev` option. You can also pass a file listing the revisions to ignore -using the `--ignore-revs-file` option. The changes made by the revision will be ignored -when assigning blame. Lines modified by an ignored revision will be blamed on the -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. - -``` -# Migrate code style to Black -5b4ab991dede475d393e9d69ec388fd6bd949699 -``` - -Afterwards, you can pass that file to `git blame` and see clean and meaningful blame -information. - -```console -$ git blame important.py --ignore-revs-file .git-blame-ignore-revs -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file): -abdfd8b0 (Alice Doe 2019-09-23 11:39:32 -0400 2) text = text.lstrip() -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3) with open(file, "r+") as f: -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4) f.write(formatted) -``` - -You can even configure `git` to automatically ignore revisions listed in a file on every -call to `git blame`. - -```console -$ git config blame.ignoreRevsFile .git-blame-ignore-revs -``` +Further information can be found in our docs: -**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!) +- [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. It also sports a decent test suite. However, it is still very -new. 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. 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. 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](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#pragmatism) +[Pragmatism](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#ast-before-and-after-formatting) section for details). If you're feeling confident, use `--fast`. ## The _Black_ code style _Black_ is a PEP 8 compliant opinionated formatter. _Black_ reformats entire files in -place. It is not configurable. It doesn't take previous formatting into account. Your -main option of configuring _Black_ is that it doesn't reformat blocks that start with -`# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`. Pay -attention that `# fmt: on/off` have to be on the same level of indentation. To learn -more about _Black_'s opinions, to go -[the_black_code_style](https://github.com/psf/black/blob/master/docs/the_black_code_style.md). +place. Style configuration options are deliberately limited and rarely added. It doesn't +take previous formatting into account (see [Pragmatism](#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: + +- [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) Please refer to this document before submitting an issue. What seems like a bug might be intended behaviour. -## Pragmatism +### Pragmatism Early versions of _Black_ used to be absolutist in some respects. They took after its initial author. This was fine at the time as it made the implementation simpler and there were not many users anyway. Not many edge cases were reported. As a mature tool, -_Black_ does make some exceptions to rules it otherwise holds. This -[section](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#pragmatism) -of `the_black_code_style` describes what those exceptions are and why this is the case. +_Black_ does make some exceptions to rules it otherwise holds. + +- [The _Black_ code style: Pragmatism](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#pragmatism) Please refer to this document before submitting an issue just like with the document above. What seems like a bug might be intended behaviour. -## pyproject.toml +## Configuration _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude`/`--extend-exclude` patterns for your project. - -**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is -"No". _Black_ is all about sensible defaults. - -### What on Earth is a `pyproject.toml` file? - -[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. - -### Where _Black_ looks for the file - -By default _Black_ looks for `pyproject.toml` starting from the common base directory of -all files and directories passed on the command line. If it's not there, it looks in -parent directories. It stops looking when it finds the file, or a `.git` directory, or a -`.hg` directory, or the root of the file system, whichever comes first. - -If you're formatting standard input, _Black_ will look for configuration starting from -the current working directory. - -You can use a "global" configuration, stored in a specific location in your home -directory. This will be used as a fallback configuration, that is, it will be used if -and only if _Black_ doesn't find any configuration as mentioned above. Depending on your -operating system, this configuration file should be stored as: - -- Windows: `~\.black` -- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the - `XDG_CONFIG_HOME` environment variable is not set) - -Note that these are paths to the TOML file itself (meaning that they shouldn't be named -as `pyproject.toml`), not directories where you store the configuration. Here, `~` -refers to the path to your home directory. On Windows, this will be something like -`C:\\Users\UserName`. - -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. - -Please note `blackd` will not use `pyproject.toml` configuration. - -### Configuration format - -As the file extension suggests, `pyproject.toml` is a -[TOML](https://github.com/toml-lang/toml) file. It contains separate sections for -different tools. _Black_ is using the `[tool.black]` section. The option keys are the -same as long names of options on the command line. - -Note that you have to use single-quoted strings in TOML for regular expressions. It's -the equivalent of r-strings in Python. Multiline strings are treated as verbose regular -expressions by Black. Use `[ ]` to denote a significant space character. +`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your +project. -

-Example pyproject.toml +You can find more details in our documentation: -```toml -[tool.black] -line-length = 88 -target-version = ['py37'] -include = '\.pyi?$' -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) -''' -``` - -
- -### Lookup hierarchy - -Command-line options have defaults that you can see in `--help`. A `pyproject.toml` can -override those defaults. Finally, options provided by the user on the command line -override both. - -_Black_ will only ever use one `pyproject.toml` file during an entire run. It doesn't -look for multiple files, and doesn't compose configuration from different levels of the -file hierarchy. - -## Editor integration - -_Black_ can be integrated into many editors with plugins. They let you run _Black_ on -your code with the ease of doing it in your editor. To get started using _Black_ in your -editor of choice, please see -[editor_integration](https://github.com/psf/black/blob/master/docs/editor_integration.md). - -Patches are welcome for editors without an editor integration or plugin! More -information can be found in -[editor_integration](https://github.com/psf/black/blob/master/docs/editor_integration.md#other-editors). - -## blackd - -`blackd` is a small HTTP server that exposes Black's functionality over a simple -protocol. The main benefit of using it is to avoid paying the cost of starting up a new -Black process every time you want to blacken a file. Please refer to -[blackd](https://github.com/psf/black/blob/master/docs/blackd.md) to get the ball -rolling. - -## black-primer - -`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of -(configured in `primer.json`) Git accessible projects in parallel. -[black_primer](https://github.com/psf/black/blob/master/docs/black_primer.md) has more -information regarding its usage and configuration. - -(A PR adding Mercurial support will be accepted.) - -## Version control integration - -Use [pre-commit](https://pre-commit.com/). Once you -[have it installed](https://pre-commit.com/#install), add this to the -`.pre-commit-config.yaml` in your repository: - -```yaml -repos: - - repo: https://github.com/psf/black - rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags - hooks: - - id: black - language_version: python3 # Should be a command that runs python3.6+ -``` - -Then run `pre-commit install` and you're ready to go. - -Avoid using `args` in the hook. Instead, store necessary configuration in -`pyproject.toml` so that editors and command-line usage of Black all behave consistently -for your project. See _Black_'s own -[pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an -example. +- [The basics: Configuration via a file](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file) -If you're already using Python 3.7, switch the `language_version` accordingly. Finally, -`stable` is a branch that tracks the latest release on PyPI. If you'd rather run on -master, this is also an option. +And if you're looking for more general configuration documentation: -## GitHub Actions +- [Usage and Configuration](https://black.readthedocs.io/en/stable/usage_and_configuration/index.html) -Create a file named `.github/workflows/black.yml` inside your repository with: - -```yaml -name: Lint - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable -``` - -You may use `options` (Default is `'--check --diff'`) and `src` (Default is `'.'`) as -follows: - -```yaml -- uses: psf/black@stable - with: - options: "--check --verbose" - src: "./src" -``` - -## Ignoring unmodified files - -_Black_ remembers files it has already formatted, unless the `--diff` flag is used or -code is passed via standard input. This information is stored per-user. The exact -location of the file depends on the _Black_ version and the system on which _Black_ is -run. The file is non-portable. The standard location on common operating systems is: - -- Windows: - `C:\\Users\\AppData\Local\black\black\Cache\\cache...pickle` -- macOS: - `/Users//Library/Caches/black//cache...pickle` -- Linux: - `/home//.cache/black//cache...pickle` - -`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//`. +**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is +"No". _Black_ is all about sensible defaults. Applying those defaults will have your +code in compliance with many other _Black_ formatted projects. ## Used by @@ -513,209 +182,39 @@ Looks like this: MIT -## Contributing to _Black_ +## Contributing -In terms of inspiration, _Black_ is about as configurable as _gofmt_. This is -deliberate. +Welcome! Happy to see you willing to make the project better. You can get started by +reading this: -Bug reports and fixes are always welcome! However, 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. +- [Contributing: The basics](https://black.readthedocs.io/en/latest/contributing/the_basics.html) -More details can be found in -[CONTRIBUTING](https://github.com/psf/black/blob/master/CONTRIBUTING.md). +You can also take a look at the rest of the contributing docs or talk with the +developers: + +- [Contributing documentation](https://black.readthedocs.io/en/latest/contributing/index.html) +- [IRC channel on Freenode](https://webchat.freenode.net/?channels=%23blackformatter) ## Change log -The log's become rather long. It moved to its own file. +The log has become rather long. It moved to its own file. -See [CHANGES](https://github.com/psf/black/blob/master/CHANGES.md). +See [CHANGES](https://black.readthedocs.io/en/latest/change_log.html). ## Authors -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. - -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) -- [Alex Vandiver](mailto:github@chmrr.net) -- [Allan Simon](mailto:allan.simon@supinfo.com) -- Anders-Petter Ljungquist -- [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) -- [Andrew Zhou](mailto:andrewfzhou@gmail.com) -- [Andrey](mailto:dyuuus@yandex.ru) -- [Andy Freeland](mailto:andy@andyfreeland.net) -- [Anthony Sottile](mailto:asottile@umich.edu) -- [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) -- [Arnav Borbornah](mailto:arnavborborah11@gmail.com) -- [Artem Malyshev](mailto:proofit404@gmail.com) -- [Asger Hautop Drewsen](mailto:asgerdrewsen@gmail.com) -- [Augie Fackler](mailto:raf@durin42.com) -- [Aviskar KC](mailto:aviskarkc10@gmail.com) -- Batuhan Taşkaya -- [Benjamin Wohlwend](mailto:bw@piquadrat.ch) -- [Benjamin Woodruff](mailto:github@benjam.info) -- [Bharat Raghunathan](mailto:bharatraghunthan9767@gmail.com) -- [Brandt Bucher](mailto:brandtbucher@gmail.com) -- [Brett Cannon](mailto:brett@python.org) -- [Bryan Bugyi](mailto:bryan.bugyi@rutgers.edu) -- [Bryan Forbes](mailto:bryan@reigndropsfall.net) -- [Calum Lind](mailto:calumlind@gmail.com) -- [Charles](mailto:peacech@gmail.com) -- Charles Reid -- [Christian Clauss](mailto:cclauss@bluewin.ch) -- [Christian Heimes](mailto:christian@python.org) -- [Chuck Wooters](mailto:chuck.wooters@microsoft.com) -- [Chris Rose](mailto:offline@offby1.net) -- Codey Oxley -- [Cong](mailto:congusbongus@gmail.com) -- [Cooper Ry Lees](mailto:me@cooperlees.com) -- [Dan Davison](mailto:dandavison7@gmail.com) -- [Daniel Hahler](mailto:github@thequod.de) -- [Daniel M. Capella](mailto:polycitizen@gmail.com) -- Daniele Esposti -- [David Hotham](mailto:david.hotham@metaswitch.com) -- [David Lukes](mailto:dafydd.lukes@gmail.com) -- [David Szotten](mailto:davidszotten@gmail.com) -- [Denis Laxalde](mailto:denis@laxalde.org) -- [Douglas Thor](mailto:dthor@transphormusa.com) -- dylanjblack -- [Eli Treuherz](mailto:eli@treuherz.com) -- [Emil Hessman](mailto:emil@hessman.se) -- [Felix Kohlgrüber](mailto:felix.kohlgrueber@gmail.com) -- [Florent Thiery](mailto:fthiery@gmail.com) -- Francisco -- [Giacomo Tagliabue](mailto:giacomo.tag@gmail.com) -- [Greg Gandenberger](mailto:ggandenberger@shoprunner.com) -- [Gregory P. Smith](mailto:greg@krypto.org) -- Gustavo Camargo -- hauntsaninja -- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) -- [Heaford](mailto:dan@heaford.com) -- [Hugo Barrera](mailto::hugo@barrera.io) -- Hugo van Kemenade -- [Hynek Schlawack](mailto:hs@ox.cx) -- [Ivan Katanić](mailto:ivan.katanic@gmail.com) -- [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) -- [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) -- [Jan Hnátek](mailto:jan.hnatek@gmail.com) -- [Jason Fried](mailto:me@jasonfried.info) -- [Jason Friedland](mailto:jason@friedland.id.au) -- [jgirardet](mailto:ijkl@netc.fr) -- Jim Brännlund -- [Jimmy Jia](mailto:tesrin@gmail.com) -- [Joe Antonakakis](mailto:jma353@cornell.edu) -- [Jon Dufresne](mailto:jon.dufresne@gmail.com) -- [Jonas Obrist](mailto:ojiidotch@gmail.com) -- [Jonty Wareing](mailto:jonty@jonty.co.uk) -- [Jose Nazario](mailto:jose.monkey.org@gmail.com) -- [Joseph Larson](mailto:larson.joseph@gmail.com) -- [Josh Bode](mailto:joshbode@fastmail.com) -- [Josh Holland](mailto:anowlcalledjosh@gmail.com) -- [Joshua Cannon](mailto:joshdcannon@gmail.com) -- [José Padilla](mailto:jpadilla@webapplicate.com) -- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) -- [kaiix](mailto:kvn.hou@gmail.com) -- [Katie McLaughlin](mailto:katie@glasnt.com) -- Katrin Leinweber -- [Keith Smiley](mailto:keithbsmiley@gmail.com) -- [Kenyon Ralph](mailto:kenyon@kenyonralph.com) -- [Kevin Kirsche](mailto:Kev.Kirsche+GitHub@gmail.com) -- [Kyle Hausmann](mailto:kyle.hausmann@gmail.com) -- [Kyle Sunden](mailto:sunden@wisc.edu) -- Lawrence Chan -- [Linus Groh](mailto:mail@linusgroh.de) -- [Loren Carvalho](mailto:comradeloren@gmail.com) -- [Luka Sterbic](mailto:luka.sterbic@gmail.com) -- [LukasDrude](mailto:mail@lukas-drude.de) -- Mahmoud Hossam -- Mariatta -- [Matt VanEseltine](mailto:vaneseltine@gmail.com) -- [Matthew Clapp](mailto:itsayellow+dev@gmail.com) -- [Matthew Walster](mailto:matthew@walster.org) -- Max Smolens -- [Michael Aquilina](mailto:michaelaquilina@gmail.com) -- [Michael Flaxman](mailto:michael.flaxman@gmail.com) -- [Michael J. Sullivan](mailto:sully@msully.net) -- [Michael McClimon](mailto:michael@mcclimon.org) -- [Miguel Gaiowski](mailto:miggaiowski@gmail.com) -- [Mike](mailto:roshi@fedoraproject.org) -- [mikehoyio](mailto:mikehoy@gmail.com) -- [Min ho Kim](mailto:minho42@gmail.com) -- [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) -- MomIsBestFriend -- [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) -- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) -- [Neraste](mailto:neraste.herr10@gmail.com) -- [Nikolaus Waxweiler](mailto:madigens@gmail.com) -- [Ofek Lev](mailto:ofekmeister@gmail.com) -- [Osaetin Daniel](mailto:osaetindaniel@gmail.com) -- [otstrel](mailto:otstrel@gmail.com) -- [Pablo Galindo](mailto:Pablogsal@gmail.com) -- [Paul Ganssle](mailto:p.ganssle@gmail.com) -- [Paul Meinhardt](mailto:mnhrdt@gmail.com) -- [Peter Bengtsson](mailto:mail@peterbe.com) -- [Peter Grayson](mailto:pete@jpgrayson.net) -- [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) -- pmacosta -- [Quentin Pradet](mailto:quentin@pradet.me) -- [Ralf Schmitt](mailto:ralf@systemexit.de) -- [Ramón Valles](mailto:mroutis@protonmail.com) -- [Richard Fearn](mailto:richardfearn@gmail.com) -- Richard Si -- [Rishikesh Jha](mailto:rishijha424@gmail.com) -- [Rupert Bedford](mailto:rupert@rupertb.com) -- Russell Davis -- [Rémi Verschelde](mailto:rverschelde@gmail.com) -- [Sami Salonen](mailto:sakki@iki.fi) -- [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) -- [Sanket Dasgupta](mailto:sanketdasgupta@gmail.com) -- Sergi -- [Scott Stevenson](mailto:scott@stevenson.io) -- Shantanu -- [shaoran](mailto:shaoran@sakuranohana.org) -- [Shinya Fujino](mailto:shf0811@gmail.com) -- springstan -- [Stavros Korokithakis](mailto:hi@stavros.io) -- [Stephen Rosen](mailto:sirosen@globus.org) -- [Steven M. Vascellaro](mailto:S.Vascellaro@gmail.com) -- [Sunil Kapil](mailto:snlkapil@gmail.com) -- [Sébastien Eustace](mailto:sebastien.eustace@gmail.com) -- [Tal Amuyal](mailto:TalAmuyal@gmail.com) -- [Terrance](mailto:git@terrance.allofti.me) -- [Thom Lu](mailto:thomas.c.lu@gmail.com) -- [Thomas Grainger](mailto:tagrain@gmail.com) -- [Tim Gates](mailto:tim.gates@iress.com) -- [Tim Swast](mailto:swast@google.com) -- [Timo](mailto:timo_tk@hotmail.com) -- Toby Fleming -- [Tom Christie](mailto:tom@tomchristie.com) -- [Tony Narlock](mailto:tony@git-pull.com) -- [Tsuyoshi Hombashi](mailto:tsuyoshi.hombashi@gmail.com) -- [Tushar Chandra](mailto:tusharchandra2018@u.northwestern.edu) -- [Tzu-ping Chung](mailto:uranusjr@gmail.com) -- [Utsav Shah](mailto:ukshah2@illinois.edu) -- utsav-dbx -- vezeli -- [Ville Skyttä](mailto:ville.skytta@iki.fi) -- [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com) -- [Vlad Emelianov](mailto:volshebnyi@gmail.com) -- [williamfzc](mailto:178894043@qq.com) -- [wouter bolsterlee](mailto:wouter@bolsterl.ee) -- Yazdan -- [Yngve Høiseth](mailto:yngve@hoiseth.net) -- [Yurii Karabas](mailto:1998uriyyo@gmail.com) -- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) +The author list is quite long nowadays, so it lives in its own file. + +See [AUTHORS.md](./AUTHORS.md) + +## Code of Conduct + +Everyone participating in the _Black_ project, and in particular in the issue tracker, +pull requests, and social media activity, is expected to treat other people with respect +and more generally to follow the guidelines articulated in the +[Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). + +At the same time, humor is encouraged. In fact, basic familiarity with Monty Python's +Flying Circus is expected. We are not savages. + +And if you _really_ need to slap somebody, do it with a fish while dancing. diff --git a/docs/authors.md b/docs/authors.md deleted file mode 100644 index 9f2ea0571b9..00000000000 --- a/docs/authors.md +++ /dev/null @@ -1,187 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# Authors - -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), and -[Cooper Lees](mailto:me@cooperlees.com). - -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) -- [Alex Vandiver](mailto:github@chmrr.net) -- [Allan Simon](mailto:allan.simon@supinfo.com) -- Anders-Petter Ljungquist -- [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) -- [Andrew Zhou](mailto:andrewfzhou@gmail.com) -- [Andrey](mailto:dyuuus@yandex.ru) -- [Andy Freeland](mailto:andy@andyfreeland.net) -- [Anthony Sottile](mailto:asottile@umich.edu) -- [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) -- [Arnav Borbornah](mailto:arnavborborah11@gmail.com) -- [Artem Malyshev](mailto:proofit404@gmail.com) -- [Asger Hautop Drewsen](mailto:asgerdrewsen@gmail.com) -- [Augie Fackler](mailto:raf@durin42.com) -- [Aviskar KC](mailto:aviskarkc10@gmail.com) -- Batuhan Taşkaya -- [Benjamin Wohlwend](mailto:bw@piquadrat.ch) -- [Benjamin Woodruff](mailto:github@benjam.info) -- [Bharat Raghunathan](mailto:bharatraghunthan9767@gmail.com) -- [Brandt Bucher](mailto:brandtbucher@gmail.com) -- [Brett Cannon](mailto:brett@python.org) -- [Bryan Bugyi](mailto:bryan.bugyi@rutgers.edu) -- [Bryan Forbes](mailto:bryan@reigndropsfall.net) -- [Calum Lind](mailto:calumlind@gmail.com) -- [Charles](mailto:peacech@gmail.com) -- Charles Reid -- [Christian Clauss](mailto:cclauss@bluewin.ch) -- [Christian Heimes](mailto:christian@python.org) -- [Chuck Wooters](mailto:chuck.wooters@microsoft.com) -- [Chris Rose](mailto:offline@offby1.net) -- Codey Oxley -- [Cong](mailto:congusbongus@gmail.com) -- [Cooper Ry Lees](mailto:me@cooperlees.com) -- [Dan Davison](mailto:dandavison7@gmail.com) -- [Daniel Hahler](mailto:github@thequod.de) -- [Daniel M. Capella](mailto:polycitizen@gmail.com) -- Daniele Esposti -- [David Hotham](mailto:david.hotham@metaswitch.com) -- [David Lukes](mailto:dafydd.lukes@gmail.com) -- [David Szotten](mailto:davidszotten@gmail.com) -- [Denis Laxalde](mailto:denis@laxalde.org) -- [Douglas Thor](mailto:dthor@transphormusa.com) -- dylanjblack -- [Eli Treuherz](mailto:eli@treuherz.com) -- [Emil Hessman](mailto:emil@hessman.se) -- [Felix Kohlgrüber](mailto:felix.kohlgrueber@gmail.com) -- [Florent Thiery](mailto:fthiery@gmail.com) -- Francisco -- [Giacomo Tagliabue](mailto:giacomo.tag@gmail.com) -- [Greg Gandenberger](mailto:ggandenberger@shoprunner.com) -- [Gregory P. Smith](mailto:greg@krypto.org) -- Gustavo Camargo -- hauntsaninja -- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) -- [Heaford](mailto:dan@heaford.com) -- [Hugo Barrera](mailto::hugo@barrera.io) -- Hugo van Kemenade -- [Hynek Schlawack](mailto:hs@ox.cx) -- [Ivan Katanić](mailto:ivan.katanic@gmail.com) -- [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) -- [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) -- [Jan Hnátek](mailto:jan.hnatek@gmail.com) -- [Jason Fried](mailto:me@jasonfried.info) -- [Jason Friedland](mailto:jason@friedland.id.au) -- [jgirardet](mailto:ijkl@netc.fr) -- Jim Brännlund -- [Jimmy Jia](mailto:tesrin@gmail.com) -- [Joe Antonakakis](mailto:jma353@cornell.edu) -- [Jon Dufresne](mailto:jon.dufresne@gmail.com) -- [Jonas Obrist](mailto:ojiidotch@gmail.com) -- [Jonty Wareing](mailto:jonty@jonty.co.uk) -- [Jose Nazario](mailto:jose.monkey.org@gmail.com) -- [Joseph Larson](mailto:larson.joseph@gmail.com) -- [Josh Bode](mailto:joshbode@fastmail.com) -- [Josh Holland](mailto:anowlcalledjosh@gmail.com) -- [José Padilla](mailto:jpadilla@webapplicate.com) -- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) -- [kaiix](mailto:kvn.hou@gmail.com) -- [Katie McLaughlin](mailto:katie@glasnt.com) -- Katrin Leinweber -- [Keith Smiley](mailto:keithbsmiley@gmail.com) -- [Kenyon Ralph](mailto:kenyon@kenyonralph.com) -- [Kevin Kirsche](mailto:Kev.Kirsche+GitHub@gmail.com) -- [Kyle Hausmann](mailto:kyle.hausmann@gmail.com) -- [Kyle Sunden](mailto:sunden@wisc.edu) -- Lawrence Chan -- [Linus Groh](mailto:mail@linusgroh.de) -- [Loren Carvalho](mailto:comradeloren@gmail.com) -- [Luka Sterbic](mailto:luka.sterbic@gmail.com) -- [LukasDrude](mailto:mail@lukas-drude.de) -- Mahmoud Hossam -- Mariatta -- [Matt VanEseltine](mailto:vaneseltine@gmail.com) -- [Matthew Clapp](mailto:itsayellow+dev@gmail.com) -- [Matthew Walster](mailto:matthew@walster.org) -- Max Smolens -- [Michael Aquilina](mailto:michaelaquilina@gmail.com) -- [Michael Flaxman](mailto:michael.flaxman@gmail.com) -- [Michael J. Sullivan](mailto:sully@msully.net) -- [Michael McClimon](mailto:michael@mcclimon.org) -- [Miguel Gaiowski](mailto:miggaiowski@gmail.com) -- [Mike](mailto:roshi@fedoraproject.org) -- [mikehoyio](mailto:mikehoy@gmail.com) -- [Min ho Kim](mailto:minho42@gmail.com) -- [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) -- MomIsBestFriend -- [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) -- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) -- [Neraste](mailto:neraste.herr10@gmail.com) -- [Nikolaus Waxweiler](mailto:madigens@gmail.com) -- [Ofek Lev](mailto:ofekmeister@gmail.com) -- [Osaetin Daniel](mailto:osaetindaniel@gmail.com) -- [otstrel](mailto:otstrel@gmail.com) -- [Pablo Galindo](mailto:Pablogsal@gmail.com) -- [Paul Ganssle](mailto:p.ganssle@gmail.com) -- [Paul Meinhardt](mailto:mnhrdt@gmail.com) -- [Paul "TBBle" Hampson](mailto:Paul.Hampson@Pobox.com) -- [Peter Bengtsson](mailto:mail@peterbe.com) -- [Peter Grayson](mailto:pete@jpgrayson.net) -- [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) -- pmacosta -- [Quentin Pradet](mailto:quentin@pradet.me) -- [Ralf Schmitt](mailto:ralf@systemexit.de) -- [Ramón Valles](mailto:mroutis@protonmail.com) -- [Richard Fearn](mailto:richardfearn@gmail.com) -- Richard Si -- [Rishikesh Jha](mailto:rishijha424@gmail.com) -- [Rupert Bedford](mailto:rupert@rupertb.com) -- Russell Davis -- [Rémi Verschelde](mailto:rverschelde@gmail.com) -- [Sami Salonen](mailto:sakki@iki.fi) -- [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) -- [Sanket Dasgupta](mailto:sanketdasgupta@gmail.com) -- Sergi -- [Scott Stevenson](mailto:scott@stevenson.io) -- Shantanu -- [shaoran](mailto:shaoran@sakuranohana.org) -- [Shinya Fujino](mailto:shf0811@gmail.com) -- springstan -- [Stavros Korokithakis](mailto:hi@stavros.io) -- [Stephen Rosen](mailto:sirosen@globus.org) -- [Steven M. Vascellaro](mailto:S.Vascellaro@gmail.com) -- [Sunil Kapil](mailto:snlkapil@gmail.com) -- [Sébastien Eustace](mailto:sebastien.eustace@gmail.com) -- [Tal Amuyal](mailto:TalAmuyal@gmail.com) -- [Terrance](mailto:git@terrance.allofti.me) -- [Thom Lu](mailto:thomas.c.lu@gmail.com) -- [Thomas Grainger](mailto:tagrain@gmail.com) -- [Tim Gates](mailto:tim.gates@iress.com) -- [Tim Swast](mailto:swast@google.com) -- [Timo](mailto:timo_tk@hotmail.com) -- Toby Fleming -- [Tom Christie](mailto:tom@tomchristie.com) -- [Tony Narlock](mailto:tony@git-pull.com) -- [Tsuyoshi Hombashi](mailto:tsuyoshi.hombashi@gmail.com) -- [Tushar Chandra](mailto:tusharchandra2018@u.northwestern.edu) -- [Tzu-ping Chung](mailto:uranusjr@gmail.com) -- [Utsav Shah](mailto:ukshah2@illinois.edu) -- utsav-dbx -- vezeli -- [Ville Skyttä](mailto:ville.skytta@iki.fi) -- [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com) -- [Vlad Emelianov](mailto:volshebnyi@gmail.com) -- [williamfzc](mailto:178894043@qq.com) -- [wouter bolsterlee](mailto:wouter@bolsterl.ee) -- Yazdan -- [Yngve Høiseth](mailto:yngve@hoiseth.net) -- [Yurii Karabas](mailto:1998uriyyo@gmail.com) -- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/docs/authors.md b/docs/authors.md new file mode 120000 index 00000000000..3234d6e0792 --- /dev/null +++ b/docs/authors.md @@ -0,0 +1 @@ +../AUTHORS.md \ No newline at end of file diff --git a/docs/black_primer.md b/docs/black_primer.md deleted file mode 100644 index a2dd964b7dc..00000000000 --- a/docs/black_primer.md +++ /dev/null @@ -1,120 +0,0 @@ -# black-primer - -`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of -(configured in `primer.json`) Git accessible projects in parallel. _(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 - -```text -Usage: black-primer [OPTIONS] - - primer - prime projects for blackening... 🏴 - -Options: - -c, --config PATH JSON config file path [default: /Users/cooper/repos/ - black/src/black_primer/primer.json] - - --debug Turn on debug logging [default: False] - -k, --keep Keep workdir + repos post run [default: False] - -L, --long-checkouts Pull big projects to test [default: False] - -R, --rebase Rebase project if already checked out [default: - False] - - -w, --workdir PATH Directory path for repo checkouts [default: /var/fol - ders/tc/hbwxh76j1hn6gqjd2n2sjn4j9k1glp/T/primer.20200 - 517125229] - - -W, --workers INTEGER Number of parallel worker coroutines [default: 2] - -h, --help Show this message and exit. -``` - -## Primer config file - -The config file is in JSON format. Its main element is the `"projects"` dictionary and -each parameter is explained below: - -```json -{ - "projects": { - "00_Example": { - "cli_arguments": "List of extra CLI arguments to pass Black for this project", - "expect_formatting_changes": "Boolean to indicate that the version of Black is expected to cause changes", - "git_clone_url": "URL you would pass `git clone` to check out this repo", - "long_checkout": "Boolean to have repo skipped by default unless `--long-checkouts` is specified", - "py_versions": "List of major Python versions to run this project with - all will do as you'd expect - run on ALL versions" - }, - "aioexabgp": { - "cli_arguments": [], - "expect_formatting_changes": true, - "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", - "long_checkout": false, - "py_versions": ["all", "3.8"] - } - } -} -``` - -An example primer config file is used by Black -[here](https://github.com/psf/black/blob/master/src/black_primer/primer.json) - -## Example run - -```console -cooper-mbp:black cooper$ ~/venvs/b/bin/black-primer -[2020-05-17 13:06:40,830] INFO: 4 projects to run Black over (lib.py:270) -[2020-05-17 13:06:44,215] INFO: Analyzing results (lib.py:285) --- primer results 📊 -- - -3 / 4 succeeded (75.0%) ✅ -1 / 4 FAILED (25.0%) 💩 - - 0 projects disabled by config - - 0 projects skipped due to Python version - - 0 skipped due to long checkout - -Failed projects: - -## flake8-bugbear: - - Returned 1 - - stdout: ---- tests/b303_b304.py 2020-05-17 20:04:09.991227 +0000 -+++ tests/b303_b304.py 2020-05-17 20:06:42.753851 +0000 -@@ -26,11 +26,11 @@ - maxint = 5 # this is okay - # the following should not crash - (a, b, c) = list(range(3)) - # it is different than this - a, b, c = list(range(3)) -- a, b, c, = list(range(3)) -+ a, b, c = list(range(3)) - # and different than this - (a, b), c = list(range(3)) - a, *b, c = [1, 2, 3, 4, 5] - b[1:3] = [0, 0] - -would reformat tests/b303_b304.py -Oh no! 💥 💔 💥 -1 file would be reformatted, 22 files would be left unchanged. -``` diff --git a/docs/conf.py b/docs/conf.py index 9e03d05e937..d5673738452 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,60 +13,11 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # from pathlib import Path -import re import string -from typing import Callable, Dict, List, Optional, Pattern, Tuple, Set -from dataclasses import dataclass -import logging from pkg_resources import get_distribution -logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO) - -LOG = logging.getLogger(__name__) - CURRENT_DIR = Path(__file__).parent -README = CURRENT_DIR / ".." / "README.md" -REFERENCE_DIR = CURRENT_DIR / "reference" -STATIC_DIR = CURRENT_DIR / "_static" - - -@dataclass -class SrcRange: - """Tracks which part of a file to get a section's content. - - Data: - start_line: The line where the section starts (i.e. its sub-header) (inclusive). - end_line: The line where the section ends (usually next sub-header) (exclusive). - """ - - start_line: int - end_line: int - - -@dataclass -class DocSection: - """Tracks information about a section of documentation. - - Data: - name: The section's name. This will used to detect duplicate sections. - src: The filepath to get its contents. - processors: The processors to run before writing the section to CURRENT_DIR. - out_filename: The filename to use when writing the section to CURRENT_DIR. - src_range: The line range of SRC to gets its contents. - """ - - name: str - src: Path - src_range: SrcRange = SrcRange(0, 1_000_000) - out_filename: str = "" - processors: Tuple[Callable, ...] = () - - def get_out_filename(self) -> str: - if not self.out_filename: - return self.name + ".md" - else: - return self.out_filename def make_pypi_svg(version: str) -> None: @@ -78,131 +29,10 @@ def make_pypi_svg(version: str) -> None: f.write(svg) -def make_filename(line: str) -> str: - non_letters: Pattern = re.compile(r"[^a-z]+") - filename: str = line[3:].rstrip().lower() - filename = non_letters.sub("_", filename) - if filename.startswith("_"): - filename = filename[1:] - if filename.endswith("_"): - filename = filename[:-1] - return filename + ".md" - - -def get_contents(section: DocSection) -> str: - """Gets the contents for the DocSection.""" - contents: List[str] = [] - src: Path = section.src - start_line: int = section.src_range.start_line - end_line: int = section.src_range.end_line - with open(src, "r", encoding="utf-8") as f: - for lineno, line in enumerate(f, start=1): - if lineno >= start_line and lineno < end_line: - contents.append(line) - result = "".join(contents) - # Let's make Prettier happy with the amount of trailing newlines in the sections. - if result.endswith("\n\n"): - result = result[:-1] - if not result.endswith("\n"): - result = result + "\n" - return result - - -def get_sections_from_readme() -> List[DocSection]: - """Gets the sections from README so they can be processed by process_sections. - - It opens README and goes down line by line looking for sub-header lines which - denotes a section. Once it finds a sub-header line, it will create a DocSection - object with all of the information currently available. Then on every line, it will - track the ending line index of the section. And it repeats this for every sub-header - line it finds. - """ - sections: List[DocSection] = [] - section: Optional[DocSection] = None - with open(README, "r", encoding="utf-8") as f: - for lineno, line in enumerate(f, start=1): - if line.startswith("## "): - filename = make_filename(line) - section_name = filename[:-3] - section = DocSection( - name=str(section_name), - src=README, - src_range=SrcRange(lineno, lineno), - out_filename=filename, - processors=(fix_headers,), - ) - sections.append(section) - if section is not None: - section.src_range.end_line += 1 - return sections - - -def fix_headers(contents: str) -> str: - """Fixes the headers of sections copied from README. - - Removes one octothorpe (#) from all headers since the contents are no longer nested - in a root document (i.e. the README). - """ - lines: List[str] = contents.splitlines() - fixed_contents: List[str] = [] - for line in lines: - if line.startswith("##"): - line = line[1:] - fixed_contents.append(line + "\n") # splitlines strips the leading newlines - return "".join(fixed_contents) - - -def process_sections( - custom_sections: List[DocSection], readme_sections: List[DocSection] -) -> None: - """Reads, processes, and writes sections to CURRENT_DIR. - - For each section, the contents will be fetched, processed by processors - required by the section, and written to CURRENT_DIR. If it encounters duplicate - sections (i.e. shares the same name attribute), it will skip processing the - duplicates. - - It processes custom sections before the README generated sections so sections in the - README can be overwritten with custom options. - """ - processed_sections: Dict[str, DocSection] = {} - modified_files: Set[Path] = set() - sections: List[DocSection] = custom_sections - sections.extend(readme_sections) - for section in sections: - if section.name in processed_sections: - LOG.warning( - f"Skipping '{section.name}' from '{section.src}' as it is a duplicate" - f" of a custom section from '{processed_sections[section.name].src}'" - ) - continue - - LOG.info(f"Processing '{section.name}' from '{section.src}'") - target_path: Path = CURRENT_DIR / section.get_out_filename() - if target_path in modified_files: - LOG.warning( - f"{target_path} has been already written to, its contents will be" - " OVERWRITTEN and notices will be duplicated" - ) - contents: str = get_contents(section) - - # processors goes here - if fix_headers in section.processors: - contents = fix_headers(contents) - - with open(target_path, "w", encoding="utf-8") as f: - if section.src.suffix == ".md" and section.src != target_path: - rel = section.src.resolve().relative_to(CURRENT_DIR.parent) - f.write(f'[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM {rel}"\n\n') - f.write(contents) - processed_sections[section.name] = section - modified_files.add(target_path) - - # -- Project information ----------------------------------------------------- project = "Black" -copyright = "2020, Łukasz Langa and contributors to Black" +copyright = "2018-Present, Łukasz Langa and contributors to Black" author = "Łukasz Langa and contributors to Black" # Autopopulate version @@ -213,33 +43,7 @@ def process_sections( for sp in "abcfr": version = version.split(sp)[0] -custom_sections = [ - DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md"), - DocSection("editor_integration", CURRENT_DIR / "editor_integration.md"), - DocSection("blackd", CURRENT_DIR / "blackd.md"), - DocSection("black_primer", CURRENT_DIR / "black_primer.md"), - DocSection("contributing_to_black", CURRENT_DIR / ".." / "CONTRIBUTING.md"), -] - -# Sphinx complains when there is a source file that isn't referenced in any of the docs. -# Since some sections autogenerated from the README are unused warnings will appear. -# -# Sections must be listed to what their name is when passed through make_filename(). -blocklisted_sections_from_readme = { - "license", - "pragmatism", - "testimonials", - "used_by", - "change_log", -} - make_pypi_svg(release) -readme_sections = get_sections_from_readme() -readme_sections = [ - x for x in readme_sections if x.name not in blocklisted_sections_from_readme -] - -process_sections(custom_sections, readme_sections) # -- General configuration --------------------------------------------------- @@ -254,11 +58,11 @@ def process_sections( "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", - "recommonmark", + "myst_parser", ] # If you need extensions of a certain version or higher, list them here. -needs_extensions = {"recommonmark": "0.5"} +needs_extensions = {"myst_parser": "0.13.7"} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -286,6 +90,17 @@ def process_sections( # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" +# We need headers to be linkable to so ask MyST-Parser to autogenerate anchor IDs for +# headers up to and including level 3. +myst_heading_anchors = 3 + +# Prettier support formatting some MyST syntax but not all, so let's disable the +# unsupported yet still enabled by default ones. +myst_disable_syntax = [ + "myst_block_break", + "myst_line_comment", + "math_block", +] # -- Options for HTML output ------------------------------------------------- @@ -299,7 +114,6 @@ def process_sections( "about.html", "navigation.html", "relations.html", - "sourcelink.html", "searchbox.html", ] } @@ -341,21 +155,6 @@ def process_sections( # -- Options for LaTeX output ------------------------------------------------ -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md new file mode 100644 index 00000000000..6b70e0bc9bc --- /dev/null +++ b/docs/contributing/gauging_changes.md @@ -0,0 +1,61 @@ +# Gauging changes + +A lot of the time, your change will affect formatting and/or performance. Quantifying +these changes is hard, so we have tooling to help make it easier. + +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 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 + +```text +Usage: black-primer [OPTIONS] + + primer - prime projects for blackening... 🏴 + +Options: + -c, --config PATH JSON config file path [default: /Users/cooper/repos/ + black/src/black_primer/primer.json] + + --debug Turn on debug logging [default: False] + -k, --keep Keep workdir + repos post run [default: False] + -L, --long-checkouts Pull big projects to test [default: False] + -R, --rebase Rebase project if already checked out [default: + False] + + -w, --workdir PATH Directory path for repo checkouts [default: /var/fol + ders/tc/hbwxh76j1hn6gqjd2n2sjn4j9k1glp/T/primer.20200 + 517125229] + + -W, --workers INTEGER Number of parallel worker coroutines [default: 2] + -h, --help Show this message and exit. +``` diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst new file mode 100644 index 00000000000..68dfcd60c28 --- /dev/null +++ b/docs/contributing/index.rst @@ -0,0 +1,39 @@ +Contributing +============ + +.. toctree:: + :hidden: + + the_basics + gauging_changes + 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*. This is +deliberate. + +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:`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/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst similarity index 100% rename from docs/reference/reference_classes.rst rename to docs/contributing/reference/reference_classes.rst diff --git a/docs/reference/reference_exceptions.rst b/docs/contributing/reference/reference_exceptions.rst similarity index 100% rename from docs/reference/reference_exceptions.rst rename to docs/contributing/reference/reference_exceptions.rst diff --git a/docs/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst similarity index 100% rename from docs/reference/reference_functions.rst rename to docs/contributing/reference/reference_functions.rst diff --git a/docs/reference/reference_summary.rst b/docs/contributing/reference/reference_summary.rst similarity index 51% rename from docs/reference/reference_summary.rst rename to docs/contributing/reference/reference_summary.rst index 780a4b46ed8..f6ff4681557 100644 --- a/docs/reference/reference_summary.rst +++ b/docs/contributing/reference/reference_summary.rst @@ -1,6 +1,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! + *Contents are subject to change.* .. toctree:: diff --git a/docs/contributing_to_black.md b/docs/contributing/the_basics.md similarity index 68% rename from docs/contributing_to_black.md rename to docs/contributing/the_basics.md index b911b465afd..461bff96505 100644 --- a/docs/contributing_to_black.md +++ b/docs/contributing/the_basics.md @@ -1,28 +1,11 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CONTRIBUTING.md" +# The basics -# Contributing to _Black_ - -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? - -## Bird's eye view - -In terms of inspiration, _Black_ is about as configurable as _gofmt_. This is -deliberate. - -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. +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. I am using macOS myself and CentOS at work. +You can use any operating system. Install all development dependencies using: @@ -63,7 +46,7 @@ $ black-primer [-k -w /tmp/black_test_repos] ### News / Changelog Requirement `Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If -you feel this PR not require a changelog entry please state that in a comment and a +you feel this PR does not require a changelog entry please state that in a comment and a maintainer can add a `skip news` label to make the CI pass. Otherwise, please ensure you have a line in the following format: @@ -71,11 +54,17 @@ have a line in the following format: - `Black` is now more awesome (#X) ``` -To workout X, please use +Note that X should be your PR number, not issue number! To workout X, please use [Next PR Number](https://ichard26.github.io/next-pr-number/?owner=psf&name=black). This is not perfect but saves a lot of release overhead as now the releaser does not need to go back and workout what to add to the `CHANGES.md` for each release. +### Style Changes + +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. + ### Docs Testing If you make changes to docs, you can test they still build locally too. @@ -96,7 +85,7 @@ your PR. You may need to change configuration for it to pass. For more `black-primer` information visit the -[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). +[documentation](./gauging_changes.md#black-primer). ## Hygiene diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 00000000000..a509d34e903 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,49 @@ +# Getting Started + +New to _Black_? Don't worry, you've found the perfect place to get started! + +## Do you like the _Black_ code style? + +Before using _Black_ on some of your code, it might be a good idea to first understand +how _Black_ will format your code. _Black_ isn't for everyone and you may find something +that is a dealbreaker for you personally, which is okay! The current _Black_ code style +[is described here](./the_black_code_style/current_style.md). + +## Try it out online + +Also, you can try out _Black_ online for minimal fuss on the +[Black Playground](https://black.vercel.app) generously created by José Padilla. + +## 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 can't wait for the latest _hotness_ and want to install from GitHub, use: + +`pip install git+git://github.com/psf/black` + +## Basic usage + +To get started right away with sensible defaults: + +```sh +black {source_file_or_directory}... +``` + +You can run _Black_ as a package if running it as a script doesn't work: + +```sh +python -m black {source_file_or_directory}... +``` + +## Next steps + +Took a look at [the _Black_ code style](./the_black_code_style/current_style.md) and +tried out _Black_? Fantastic, you're ready for more. Why not explore some more on using +_Black_ by reading +[Usage and Configuration: The basics](./usage_and_configuration/the_basics.md). +Alternatively, you can check out the +[Introducing _Black_ to your project](./guides/introducing_black_to_your_project.md) +guide. diff --git a/docs/github_actions.md b/docs/github_actions.md deleted file mode 100644 index bd4680998d9..00000000000 --- a/docs/github_actions.md +++ /dev/null @@ -1,27 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# GitHub Actions - -Create a file named `.github/workflows/black.yml` inside your repository with: - -```yaml -name: Lint - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable - with: - black_args: ". --check" -``` - -## Inputs - -### `black_args` - -**optional**: Black input arguments. Defaults to `. --check --diff`. diff --git a/docs/guides/index.rst b/docs/guides/index.rst new file mode 100644 index 00000000000..717c5c4d066 --- /dev/null +++ b/docs/guides/index.rst @@ -0,0 +1,14 @@ +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/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md new file mode 100644 index 00000000000..71ccf7c114c --- /dev/null +++ b/docs/guides/introducing_black_to_your_project.md @@ -0,0 +1,50 @@ +# Introducing _Black_ to your project + +```{note} +This guide is incomplete. Contributions are welcomed and would be deeply +appreciated! +``` + +## Avoiding ruining git blame + +A long-standing argument against moving to automated code formatters like _Black_ is +that the migration will clutter up the output of `git blame`. This was a valid argument, +but since Git version 2.23, Git natively supports +[ignoring revisions in blame](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt) +with the `--ignore-rev` option. You can also pass a file listing the revisions to ignore +using the `--ignore-revs-file` option. The changes made by the revision will be ignored +when assigning blame. Lines modified by an ignored revision will be blamed on the +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. + +```text +# Migrate code style to Black +5b4ab991dede475d393e9d69ec388fd6bd949699 +``` + +Afterwards, you can pass that file to `git blame` and see clean and meaningful blame +information. + +```console +$ git blame important.py --ignore-revs-file .git-blame-ignore-revs +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file): +abdfd8b0 (Alice Doe 2019-09-23 11:39:32 -0400 2) text = text.lstrip() +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3) with open(file, "r+") as f: +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4) f.write(formatted) +``` + +You can even configure `git` to automatically ignore revisions listed in a file on every +call to `git blame`. + +```console +$ 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!) diff --git a/docs/compatible_configs.md b/docs/guides/using_black_with_other_tools.md similarity index 92% rename from docs/compatible_configs.md rename to docs/guides/using_black_with_other_tools.md index de81769f72d..4b22f670fe1 100644 --- a/docs/compatible_configs.md +++ b/docs/guides/using_black_with_other_tools.md @@ -1,4 +1,6 @@ -# _Black_ compatible configurations +# Using _Black_ with other tools + +## Black compatible configurations All of Black's changes are harmless (or at least, they should be), but a few do conflict against other tools. It is not uncommon to be using other tools alongside _Black_ like @@ -13,13 +15,13 @@ tools, using **their** supported file formats. Compatible configuration files can be [found here](https://github.com/psf/black/blob/master/docs/compatible_configs/). -## isort +### isort [isort](https://pypi.org/p/isort/) helps to sort and format imports in your Python code. _Black_ also formats imports, but in a different way from isort's defaults which leads to conflicting changes. -### Profile +#### Profile Since version 5.0.0, isort supports [profiles](https://pycqa.github.io/isort/docs/configuration/profiles/) to allow easy @@ -32,7 +34,7 @@ by isort. Below, an example for `pyproject.toml`: profile = "black" ``` -### Custom Configuration +#### Custom Configuration If you're using an isort version that is older than 5.0.0 or you have some custom configuration for _Black_, you can tweak your isort configuration to make it compatible @@ -47,12 +49,12 @@ ensure_newline_before_comments = True line_length = 88 ``` -### Why those options above? +#### 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 -[found here](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#how-black-wraps-lines). +[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 "Grid". @@ -90,7 +92,7 @@ works the same as with _Black_. **Please note** `ensure_newline_before_comments = True` only works since isort >= 5 but does not break older versions so you can keep it if you are running previous versions. -### Formats +#### Formats
.isort.cfg @@ -132,21 +134,21 @@ profile = black
-## Flake8 +### Flake8 [Flake8](https://pypi.org/p/flake8/) is a code linter. It warns you of syntax errors, possible bugs, stylistic errors, etc. For the most part, Flake8 follows [PEP 8](https://www.python.org/dev/peps/pep-0008/) when warning about stylistic errors. There are a few deviations that cause incompatibilities with _Black_. -### Configuration +#### Configuration ``` max-line-length = 88 extend-ignore = E203 ``` -### Why those options above? +#### Why those options above? In some cases, as determined by PEP 8, _Black_ will enforce an equal amount of whitespace around slice operators. Due to this, Flake8 will raise @@ -163,7 +165,7 @@ in your configuration. Also, as like with isort, flake8 should be configured to allow lines up to the length limit of `88`, _Black_'s default. This explains `max-line-length = 88`. -### Formats +#### Formats
.flake8 @@ -198,24 +200,24 @@ extend-ignore = E203
-## Pylint +### Pylint [Pylint](https://pypi.org/p/pylint/) is also a code linter like Flake8. It has the same checks as flake8 and more. In particular, it has more formatting checks regarding style conventions like variable naming. With so many checks, Pylint is bound to have some mixed feelings about _Black_'s formatting style. -### Configuration +#### Configuration ``` disable = C0330, C0326 max-line-length = 88 ``` -### Why those options above? +#### Why those options above? When _Black_ is folding very long expressions, the closing brackets will -[be dedented](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#how-black-wraps-lines). +[be dedented](../the_black_code_style/current_style.md#how-black-wraps-lines). ```py3 ImportantClass.important_method( @@ -223,7 +225,7 @@ ImportantClass.important_method( ) ``` -Although, this style is PEP 8 compliant, Pylint will raise +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`. @@ -234,7 +236,7 @@ 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`. -### Formats +#### Formats
pylintrc diff --git a/docs/index.rst b/docs/index.rst index f03d247d949..a7a7160f71f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,12 +14,27 @@ 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 `_. +Try it out now using the `Black Playground `_. -.. note:: +.. admonition:: Note - this is a beta product + + *Black* is already `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 versio + 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. - `Black is beta `_. + 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 ------------ @@ -42,28 +57,62 @@ 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: 2 - - installation_and_usage - the_black_code_style - pyproject_toml - compatible_configs - editor_integration - blackd - black_primer - version_control_integration - github_actions - ignoring_unmodified_files - contributing_to_black - show_your_style + :maxdepth: 3 + :includehidden: + + the_black_code_style/index + +.. toctree:: + :maxdepth: 3 + :includehidden: + + getting_started + usage_and_configuration/index + integrations/index + guides/index + +.. toctree:: + :maxdepth: 3 + :includehidden: + + contributing/index change_log - reference/reference_summary authors +.. toctree:: + :hidden: + + GitHub ↪ + PyPI ↪ + IRC ↪ + Indices and tables ================== diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md deleted file mode 100644 index fcde49f4e3a..00000000000 --- a/docs/installation_and_usage.md +++ /dev/null @@ -1,195 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# Installation and usage - -## Installation - -_Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to -run but you can reformat Python 2 code with it, too. - -### Install from GitHub - -If you can't wait for the latest _hotness_ and want to install from GitHub, use: - -`pip install git+git://github.com/psf/black` - -## Usage - -To get started right away with sensible defaults: - -```sh -black {source_file_or_directory} -``` - -You can run _Black_ as a package if running it as a script doesn't work: - -```sh -python -m black {source_file_or_directory} -``` - -## Command line options - -_Black_ doesn't provide many options. You can list them by running `black --help`: - -```text -Usage: black [OPTIONS] [SRC]... - - The uncompromising code formatter. - -Options: - -c, --code TEXT Format the code passed in as a string. - -l, --line-length INTEGER How many characters per line to allow. - [default: 88] - - -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] - Python versions that should be supported by - Black's output. [default: per-file auto- - detection] - - --pyi Format all input files like typing stubs - regardless of file extension (useful when - piping source on standard input). - - -S, --skip-string-normalization - Don't normalize string quotes or prefixes. - -C, --skip-magic-trailing-comma - Don't use trailing commas as a reason to - split lines. - - --check Don't write the files back, just return the - status. Return code 0 means nothing would - change. Return code 1 means some files - would be reformatted. Return code 123 means - there was an internal error. - - --diff Don't write the files back, just output a - diff for each file on stdout. - - --color / --no-color Show colored diff. Only applies when - `--diff` is given. - - --fast / --safe If --fast given, skip temporary sanity - checks. [default: --safe] - - --include TEXT 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. - [default: \.pyi?$] - - --exclude TEXT 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: /(\.direnv|\.eggs|\.git|\. - hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.sv - n|_build|buck-out|build|dist)/] - - --force-exclude TEXT Like --exclude, but files and directories - matching this regex will be excluded even - when they are passed explicitly as - arguments. - - --extend-exclude TEXT Like --exclude, but adds additional files - and directories on top of the excluded - ones. (useful if you simply want to add to - the default) - - --stdin-filename TEXT 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. - - -q, --quiet Don't emit non-error messages to stderr. - Errors are still emitted; silence those with - 2>/dev/null. - - -v, --verbose Also emit messages to stderr about files - that were not changed or were ignored due to - exclusion patterns. - - --version Show the version and exit. - --config FILE Read configuration from FILE path. - -h, --help Show this message and exit. -``` - -_Black_ is a well-behaved Unix-style command-line tool: - -- it does nothing if no sources are passed to it; -- 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). - -## Using _Black_ with other tools - -While _Black_ enforces formatting that conforms to PEP 8, other tools may raise warnings -about _Black_'s changes or will overwrite _Black_'s changes. A good example of this is -[isort](https://pypi.org/p/isort). Since _Black_ is barely configurable, these tools -should be configured to neither warn about nor overwrite _Black_'s changes. - -Actual details on _Black_ compatible configurations for various tools can be found in -[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md#black-compatible-configurations). - -## Migrating your code style without ruining git blame - -A long-standing argument against moving to automated code formatters like _Black_ is -that the migration will clutter up the output of `git blame`. This was a valid argument, -but since Git version 2.23, Git natively supports -[ignoring revisions in blame](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt) -with the `--ignore-rev` option. You can also pass a file listing the revisions to ignore -using the `--ignore-revs-file` option. The changes made by the revision will be ignored -when assigning blame. Lines modified by an ignored revision will be blamed on the -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. - -``` -# Migrate code style to Black -5b4ab991dede475d393e9d69ec388fd6bd949699 -``` - -Afterwards, you can pass that file to `git blame` and see clean and meaningful blame -information. - -```console -$ git blame important.py --ignore-revs-file .git-blame-ignore-revs -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file): -abdfd8b0 (Alice Doe 2019-09-23 11:39:32 -0400 2) text = text.lstrip() -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3) with open(file, "r+") as f: -7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4) f.write(formatted) -``` - -You can even configure `git` to automatically ignore revisions listed in a file on every -call to `git blame`. - -```console -$ 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!) - -## NOTE: This is a beta product - -_Black_ is already [successfully used](https://github.com/psf/black#used-by) by many -projects, small and big. It also sports a decent test suite. However, it is still very -new. 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. - -Also, as a temporary safety measure, _Black_ will check that the reformatted code still -produces a valid AST that is equivalent to the original. This slows it down. If you're -feeling confident, use `--fast`. diff --git a/docs/editor_integration.md b/docs/integrations/editors.md similarity index 85% rename from docs/editor_integration.md rename to docs/integrations/editors.md index f2d21f21113..6a69e9e33fd 100644 --- a/docs/editor_integration.md +++ b/docs/integrations/editors.md @@ -4,7 +4,7 @@ Options include the following: -- [purcell/reformatter.el](https://github.com/purcell/reformatter.el) +- [wbolster/emacs-python-black](https://github.com/wbolster/emacs-python-black) - [proofit404/blacken](https://github.com/pythonic-emacs/blacken) - [Elpy](https://github.com/jorgenschaefer/elpy). @@ -16,7 +16,7 @@ Options include the following: $ pip install black ``` -2. Locate your `black` installation folder. +1. Locate your `black` installation folder. On macOS / Linux / BSD: @@ -35,7 +35,7 @@ Options include the following: 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`. -3. Open External tools in PyCharm/IntelliJ IDEA +1. Open External tools in PyCharm/IntelliJ IDEA On macOS: @@ -45,29 +45,29 @@ Options include the following: `File -> Settings -> Tools -> External Tools` -4. Click the + icon to add a new external tool with the following values: +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: + - Program: \ - Arguments: `"$FilePath$"` -5. Format the currently opened file by selecting `Tools -> External Tools -> black`. +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`. -6. Optionally, run _Black_ on every file save: +1. Optionally, run _Black_ on every file save: 1. Make sure you have the [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin installed. - 2. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a + 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: + - Program: \ - Arguments: `$FilePath$` - Output paths to refresh: `$FilePath$` - Working directory: `$ProjectFileDir$` @@ -87,13 +87,13 @@ Wing supports black via the OS Commands tool, as explained in the Wing documenta $ pip install black ``` -2. Make sure it runs from the command line, e.g. +1. Make sure it runs from the command line, e.g. ```console $ black --help ``` -3. In Wing IDE, activate the **OS Commands** panel and define the command **black** to +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 @@ -106,7 +106,7 @@ Wing supports black via the OS Commands tool, as explained in the Wing documenta - [x] Auto-save files before execution - [x] Line mode -4. Select a file in the editor and press **F1** , or whatever key binding you selected +1. Select a file in the editor and press **F1** , or whatever key binding you selected in step 3, to reformat the file. ## Vim @@ -238,8 +238,10 @@ $ pip install -U black --no-binary regex,typed-ast ### With ALE 1. Install [`ale`](https://github.com/dense-analysis/ale) -2. Install `black` -3. Add this to your vimrc: + +1. Install `black` + +1. Add this to your vimrc: ```vim let g:ale_fixers = {} @@ -256,10 +258,10 @@ $ gedit ``` 1. `Go to edit > preferences > plugins` -2. Search for `external tools` and activate it. -3. In `Tools menu -> Manage external tools` -4. Add a new tool using `+` button. -5. Copy the below content to the code window. +1. Search for `external tools` and activate it. +1. In `Tools menu -> Manage external tools` +1. Add a new tool using `+` button. +1. Copy the below content to the code window. ```console #!/bin/bash @@ -319,17 +321,3 @@ hook global WinSetOption filetype=python %{ ## Thonny Use [Thonny-black-code-format](https://github.com/Franccisco/thonny-black-code-format). - -## Other integrations - -Other editors and tools will require external contributions. - -Patches welcome! ✨ 🍰 ✨ - -Any tool that 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/github_actions.md b/docs/integrations/github_actions.md new file mode 100644 index 00000000000..9e8cf436453 --- /dev/null +++ b/docs/integrations/github_actions.md @@ -0,0 +1,35 @@ +# GitHub Actions integration + +You can use _Black_ within a GitHub Actions workflow without setting your own Python +environment. Great for enforcing that your code matches the _Black_ code style. + +## Usage + +Create a file named `.github/workflows/black.yml` inside your repository with: + +```yaml +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable +``` + +We recommend the use of the `@stable` tag, but per version tags also exist if you prefer +that. + +You may use `options` (Default is `'--check --diff'`) and `src` (Default is `'.'`) as +follows: + +```yaml +- uses: psf/black@stable + with: + options: "--check --verbose" + src: "./src" +``` diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst new file mode 100644 index 00000000000..ed62ebcf044 --- /dev/null +++ b/docs/integrations/index.rst @@ -0,0 +1,28 @@ +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/integrations/source_version_control.md b/docs/integrations/source_version_control.md new file mode 100644 index 00000000000..1ca6161bf0b --- /dev/null +++ b/docs/integrations/source_version_control.md @@ -0,0 +1,14 @@ +# Version control integration + +Use [pre-commit](https://pre-commit.com/). Once you +[have it installed](https://pre-commit.com/#install), add this to the +`.pre-commit-config.yaml` in your repository: + +```yaml +repos: + - repo: https://github.com/psf/black + rev: stable # Replace by any tag/version: https://github.com/psf/black/tags + hooks: + - id: black + language_version: python3 # Should be a command that runs python3.6+ +``` diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000000..2dc20a24492 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,6 @@ +:orphan: + +License +======= + +.. include:: ../LICENSE diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md deleted file mode 100644 index ed88f37aa4a..00000000000 --- a/docs/pyproject_toml.md +++ /dev/null @@ -1,91 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# pyproject.toml - -_Black_ is able to read project-specific default values for its command line options -from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your -project. - -**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is -"No". _Black_ is all about sensible defaults. - -## What on Earth is a `pyproject.toml` file? - -[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. - -## Where _Black_ looks for the file - -By default _Black_ looks for `pyproject.toml` starting from the common base directory of -all files and directories passed on the command line. If it's not there, it looks in -parent directories. It stops looking when it finds the file, or a `.git` directory, or a -`.hg` directory, or the root of the file system, whichever comes first. - -If you're formatting standard input, _Black_ will look for configuration starting from -the current working directory. - -You can use a "global" configuration, stored in a specific location in your home -directory. This will be used as a fallback configuration, that is, it will be used if -and only if _Black_ doesn't find any configuration as mentioned above. Depending on your -operating system, this configuration file should be stored as: - -- Windows: `~\.black` -- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the - `XDG_CONFIG_HOME` environment variable is not set) - -Note that these are paths to the TOML file itself (meaning that they shouldn't be named -as `pyproject.toml`), not directories where you store the configuration. Here, `~` -refers to the path to your home directory. On Windows, this will be something like -`C:\\Users\UserName`. - -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. - -Files listed within a projects `.gitignore` file will not be formatted by _Black_. - -Please note `blackd` will not use `pyproject.toml` configuration. - -## Configuration format - -As the file extension suggests, `pyproject.toml` is a -[TOML](https://github.com/toml-lang/toml) file. It contains separate sections for -different tools. _Black_ is using the `[tool.black]` section. The option keys are the -same as long names of options on the command line. - -Note that you have to use single-quoted strings in TOML for regular expressions. It's -the equivalent of r-strings in Python. Multiline strings are treated as verbose regular -expressions by Black. Use `[ ]` to denote a significant space character. - -
-Example pyproject.toml - -```toml -[tool.black] -line-length = 88 -target-version = ['py37'] -include = '\.pyi?$' -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) -''' -``` - -
- -## Lookup hierarchy - -Command-line options have defaults that you can see in `--help`. A `pyproject.toml` can -override those defaults. Finally, options provided by the user on the command line -override both. - -_Black_ will only ever use one `pyproject.toml` file during an entire run. It doesn't -look for multiple files, and doesn't compose configuration from different levels of the -file hierarchy. diff --git a/docs/requirements.txt b/docs/requirements.txt index fcb6809cadc..6c34e1fefab 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,2 @@ -recommonmark==0.6.0 -Sphinx==3.2.1 -Pygments==2.7.4 \ No newline at end of file +MyST-Parser==0.13.7 +Sphinx==3.5.4 diff --git a/docs/show_your_style.md b/docs/show_your_style.md deleted file mode 100644 index 67b213c3965..00000000000 --- a/docs/show_your_style.md +++ /dev/null @@ -1,19 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# 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: - -``` -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black -``` - -Looks like this: -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style/current_style.md similarity index 91% rename from docs/the_black_code_style.md rename to docs/the_black_code_style/current_style.md index 39a452ff9a8..7d08bc9cad5 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style/current_style.md @@ -2,11 +2,13 @@ ## Code style -_Black_ reformats entire files in place. It is not configurable. It doesn't take -previous formatting into account. It doesn't reformat blocks that start with -`# fmt: off` and end with `# fmt: on`. `# 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. +_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`. +`# 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. ### How _Black_ wraps lines @@ -75,6 +77,8 @@ def very_important_function( ... ``` +(labels/why-no-backslashes)= + _Black_ prefers parentheses over backslashes, and will remove backslashes if found. ```py3 @@ -115,29 +119,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. -However there is one exception: `with` statements using multiple context managers. -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 now 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 \ -: - ... # backslashes and an ugly stranded colon -``` - 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 @@ -289,13 +270,13 @@ 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. -As an experimental option, _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. To enable experimental -string processing, pass `--experimental-string-processing` on the command line. Because -the functionality is experimental, feedback and issue reports are highly encouraged! +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 @@ -354,7 +335,7 @@ pair of parentheses to form an atom. There are a few interesting cases: In those cases, parentheses are removed when the entire statement fits in one line, or if the inner expression doesn't have any delimiters to further split on. If there is only a single delimiter and the expression starts or ends with a bracket, the -parenthesis can also be successfully omitted since the existing bracket pair will +parentheses can also be successfully omitted since the existing bracket pair will organize the expression neatly anyway. Otherwise, the parentheses are added. Please note that _Black_ does not add or remove any additional nested parentheses that @@ -453,7 +434,7 @@ into one item per line. How do you make it stop? Just delete that trailing comma and _Black_ will collapse your collection into one line if it fits. -If you must, you can recover the behaviour of early versions of Black with the option +If you must, you can recover the behaviour of early versions of _Black_ with the option `--skip-magic-trailing-comma` / `-C`. ### r"strings" and R"strings" @@ -478,11 +459,11 @@ target. There are three limited cases in which the AST does differ: of docstrings that we're aware of sanitizes indentation and leading/trailing whitespace anyway. -2. _Black_ manages optional parentheses for some statements. In the case of the `del` +1. _Black_ manages optional parentheses for some statements. In the case of the `del` statement, presence of wrapping parentheses or lack of thereof changes the resulting AST but is semantically equivalent in the interpreter. -3. _Black_ might move comments around, which includes type comments. Those are part of +1. _Black_ might move comments around, which includes type comments. Those are part of the AST as of Python 3.8. While the tool implements a number of special cases for those comments, there is no guarantee they will remain where they were in the source. Note that this doesn't change runtime behavior of the source code. diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md new file mode 100644 index 00000000000..aca9fe04017 --- /dev/null +++ b/docs/the_black_code_style/future_style.md @@ -0,0 +1,35 @@ +# 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 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 \ +: + ... # 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. diff --git a/docs/the_black_code_style/index.rst b/docs/the_black_code_style/index.rst new file mode 100644 index 00000000000..4693437be8b --- /dev/null +++ b/docs/the_black_code_style/index.rst @@ -0,0 +1,19 @@ +The Black Code Style +==================== + +.. toctree:: + :hidden: + + Current style + Future 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! + +Documentation for both the current and future styles can be found: + +- :doc:`current_style` +- :doc:`future_style` diff --git a/docs/blackd.md b/docs/usage_and_configuration/black_as_a_server.md similarity index 98% rename from docs/blackd.md rename to docs/usage_and_configuration/black_as_a_server.md index c8058ee7c63..0c0382b3e50 100644 --- a/docs/blackd.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -1,10 +1,10 @@ -## blackd +# Black as a server (blackd) `blackd` is a small HTTP server that exposes _Black_'s functionality over a simple 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. -### Usage +## 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. @@ -36,7 +36,7 @@ blackd --bind-port 9090 & # or let blackd choose a port curl -s -XPOST "localhost:9090" -d "print('valid')" ``` -### Protocol +## Protocol `blackd` only accepts `POST` requests at the `/` path. The body of the request should contain the python source code to be formatted, encoded according to the `charset` field diff --git a/docs/ignoring_unmodified_files.md b/docs/usage_and_configuration/file_collection_and_discovery.md similarity index 61% rename from docs/ignoring_unmodified_files.md rename to docs/usage_and_configuration/file_collection_and_discovery.md index a915f4e8678..54c76cd9a0f 100644 --- a/docs/ignoring_unmodified_files.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -1,6 +1,11 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" +# File collection and discovery -# Ignoring unmodified files +You can directly pass _Black_ files, but you can also pass directories and _Black_ will +walk them, collecting files to format. It determines what files to format or skip +automatically using the inclusion and exclusion regexes and as well their modification +time. + +## Ignoring unmodified files _Black_ remembers files it has already formatted, unless the `--diff` flag is used or code is passed via standard input. This information is stored per-user. The exact @@ -21,3 +26,12 @@ To override the location of these files on macOS or Linux, set the environment v `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//`. + +## .gitignore + +If `--exclude` is not set, _Black_ will automatically ignore files and directories in +`.gitignore` file, if present. The `.gitignore` file must be in the project root to be +used and nested `.gitignore` aren't supported. + +If you want _Black_ to continue using `.gitignore` while also configuring the exclusion +rules, please use `--extend-exclude`. diff --git a/docs/usage_and_configuration/index.rst b/docs/usage_and_configuration/index.rst new file mode 100644 index 00000000000..84a9c0cb99b --- /dev/null +++ b/docs/usage_and_configuration/index.rst @@ -0,0 +1,24 @@ +Usage and Configuration +======================= + +.. toctree:: + :hidden: + + the_basics + file_collection_and_discovery + black_as_a_server + +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>` diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md new file mode 100644 index 00000000000..6fd8769b591 --- /dev/null +++ b/docs/usage_and_configuration/the_basics.md @@ -0,0 +1,227 @@ +# The basics + +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 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). + +## Usage + +To get started right away with sensible defaults: + +```sh +black {source_file_or_directory} +``` + +You can run _Black_ as a package if running it as a script doesn't work: + +```sh +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`. + +
+ +Help output + +``` + Usage: black [OPTIONS] [SRC]... + + The uncompromising code formatter. + + Options: + -c, --code TEXT Format the code passed in as a string. + -l, --line-length INTEGER How many characters per line to allow. + [default: 88] + + -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] + Python versions that should be supported by + Black's output. [default: per-file auto- + detection] + + --pyi Format all input files like typing stubs + regardless of file extension (useful when + piping source on standard input). + + -S, --skip-string-normalization + Don't normalize string quotes or prefixes. + -C, --skip-magic-trailing-comma + Don't use trailing commas as a reason to + split lines. + + --check Don't write the files back, just return the + status. Return code 0 means nothing would + change. Return code 1 means some files + would be reformatted. Return code 123 means + there was an internal error. + + --diff Don't write the files back, just output a + diff for each file on stdout. + + --color / --no-color Show colored diff. Only applies when + `--diff` is given. + + --fast / --safe If --fast given, skip temporary sanity + checks. [default: --safe] + + --include TEXT 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. + [default: \.pyi?$] + + --exclude TEXT 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: /(\.direnv|\.eggs|\.git|\. + hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_bu + ild|buck-out|build|dist)/] + + --extend-exclude TEXT 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 TEXT Like --exclude, but files and directories + matching this regex will be excluded even + when they are passed explicitly as + arguments. + + + --stdin-filename TEXT 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. + + -q, --quiet Don't emit non-error messages to stderr. + Errors are still emitted; silence those with + 2>/dev/null. + + -v, --verbose Also emit messages to stderr about files + that were not changed or were ignored due to + exclusion patterns. + + --version Show the version and exit. + --config FILE Read configuration from FILE path. + -h, --help Show this message and exit. +``` + +
+ +## Configuration via a file + +_Black_ is able to read project-specific default values for its command line options +from a `pyproject.toml` file. This is especially useful for specifying custom +`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your +project. + +**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is +"No". _Black_ is all about sensible defaults. Applying those defaults will have your +code in compliance with many other _Black_ formatted projects. + +### What on Earth is a `pyproject.toml` file? + +[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. + +### Where _Black_ looks for the file + +By default _Black_ looks for `pyproject.toml` starting from the common base directory of +all files and directories passed on the command line. If it's not there, it looks in +parent directories. It stops looking when it finds the file, or a `.git` directory, or a +`.hg` directory, or the root of the file system, whichever comes first. + +If you're formatting standard input, _Black_ will look for configuration starting from +the current working directory. + +You can use a "global" configuration, stored in a specific location in your home +directory. This will be used as a fallback configuration, that is, it will be used if +and only if _Black_ doesn't find any configuration as mentioned above. Depending on your +operating system, this configuration file should be stored as: + +- Windows: `~\.black` +- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the + `XDG_CONFIG_HOME` environment variable is not set) + +Note that these are paths to the TOML file itself (meaning that they shouldn't be named +as `pyproject.toml`), not directories where you store the configuration. Here, `~` +refers to the path to your home directory. On Windows, this will be something like +`C:\\Users\UserName`. + +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. + +Please note `blackd` will not use `pyproject.toml` configuration. + +### Configuration format + +As the file extension suggests, `pyproject.toml` is a +[TOML](https://github.com/toml-lang/toml) file. It contains separate sections for +different tools. _Black_ is using the `[tool.black]` section. The option keys are the +same as long names of options on the command line. + +Note that you have to use single-quoted strings in TOML for regular expressions. It's +the equivalent of r-strings in Python. Multiline strings are treated as verbose regular +expressions by Black. Use `[ ]` to denote a significant space character. + +
+Example pyproject.toml + +```toml +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +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) +''' +``` + +
+ +### Lookup hierarchy + +Command-line options have defaults that you can see in `--help`. A `pyproject.toml` can +override those defaults. Finally, options provided by the user on the command line +override both. + +_Black_ will only ever use one `pyproject.toml` file during an entire run. It doesn't +look for multiple files, and doesn't compose configuration from different levels of the +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). + +Another good choice would be setting up an +[integration with your editor](../integrations/editors.md) of choice or with +[pre-commit for source version control](../integrations/source_version_control.md). diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md deleted file mode 100644 index 2d8bc172eba..00000000000 --- a/docs/version_control_integration.md +++ /dev/null @@ -1,28 +0,0 @@ -[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" - -# Version control integration - -Use [pre-commit](https://pre-commit.com/). Once you -[have it installed](https://pre-commit.com/#install), add this to the -`.pre-commit-config.yaml` in your repository: - -```yaml -repos: - - repo: https://github.com/psf/black - rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags - hooks: - - id: black - language_version: python3 # Should be a command that runs python3.6+ -``` - -Then run `pre-commit install` and you're ready to go. - -Avoid using `args` in the hook. Instead, store necessary configuration in -`pyproject.toml` so that editors and command-line usage of Black all behave consistently -for your project. See _Black_'s own -[pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an -example. - -If you're already using Python 3.7, switch the `language_version` accordingly. Finally, -`stable` is a branch that tracks the latest release on PyPI. If you'd rather run on -master, this is also an option. From 1bedc176d19f0e71c24149c696c5a2a71f21e941 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 8 May 2021 15:53:01 -0400 Subject: [PATCH 233/680] Fix autodoc refs broken by refactor (#2207) --- .../reference/reference_classes.rst | 6 +- .../reference/reference_exceptions.rst | 2 +- .../reference/reference_functions.rst | 112 +++++++++--------- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/docs/contributing/reference/reference_classes.rst b/docs/contributing/reference/reference_classes.rst index 8a2ded9dfec..fa765961e69 100644 --- a/docs/contributing/reference/reference_classes.rst +++ b/docs/contributing/reference/reference_classes.rst @@ -8,7 +8,7 @@ :class:`BracketTracker` ----------------------- -.. autoclass:: black.BracketTracker +.. autoclass:: black.brackets.BracketTracker :members: :class:`EmptyLineTracker` @@ -34,7 +34,7 @@ :class:`ProtoComment` --------------------- -.. autoclass:: black.ProtoComment +.. autoclass:: black.comments.ProtoComment :members: :class:`Report` @@ -47,7 +47,7 @@ :class:`Visitor` ---------------- -.. autoclass:: black.Visitor +.. autoclass:: black.nodes.Visitor :show-inheritance: :members: diff --git a/docs/contributing/reference/reference_exceptions.rst b/docs/contributing/reference/reference_exceptions.rst index 517249fa4ce..aafe61e5017 100644 --- a/docs/contributing/reference/reference_exceptions.rst +++ b/docs/contributing/reference/reference_exceptions.rst @@ -5,7 +5,7 @@ .. currentmodule:: black -.. autoexception:: black.CannotSplit +.. autoexception:: black.linegen.CannotSplit .. autoexception:: black.NothingChanged diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index bffce7e9e32..4353d1bf9a9 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -12,31 +12,31 @@ Assertions and checks .. autofunction:: black.assert_stable -.. autofunction:: black.can_be_split +.. autofunction:: black.lines.can_be_split -.. autofunction:: black.can_omit_invisible_parens +.. autofunction:: black.lines.can_omit_invisible_parens -.. autofunction:: black.is_empty_tuple +.. autofunction:: black.nodes.is_empty_tuple -.. autofunction:: black.is_import +.. autofunction:: black.nodes.is_import -.. autofunction:: black.is_line_short_enough +.. autofunction:: black.lines.is_line_short_enough -.. autofunction:: black.is_multiline_string +.. autofunction:: black.nodes.is_multiline_string -.. autofunction:: black.is_one_tuple +.. autofunction:: black.nodes.is_one_tuple -.. autofunction:: black.is_split_after_delimiter +.. autofunction:: black.brackets.is_split_after_delimiter -.. autofunction:: black.is_split_before_delimiter +.. autofunction:: black.brackets.is_split_before_delimiter -.. autofunction:: black.is_stub_body +.. autofunction:: black.nodes.is_stub_body -.. autofunction:: black.is_stub_suite +.. autofunction:: black.nodes.is_stub_suite -.. autofunction:: black.is_vararg +.. autofunction:: black.nodes.is_vararg -.. autofunction:: black.is_yield +.. autofunction:: black.nodes.is_yield Formatting @@ -70,111 +70,109 @@ Parsing .. autofunction:: black.decode_bytes -.. autofunction:: black.lib2to3_parse +.. autofunction:: black.parsing.lib2to3_parse -.. autofunction:: black.lib2to3_unparse +.. autofunction:: black.parsing.lib2to3_unparse Split functions --------------- -.. autofunction:: black.bracket_split_build_line +.. autofunction:: black.linegen.bracket_split_build_line -.. autofunction:: black.bracket_split_succeeded_or_raise +.. autofunction:: black.linegen.bracket_split_succeeded_or_raise -.. autofunction:: black.delimiter_split +.. autofunction:: black.linegen.delimiter_split -.. autofunction:: black.left_hand_split +.. autofunction:: black.linegen.left_hand_split -.. autofunction:: black.right_hand_split +.. autofunction:: black.linegen.right_hand_split -.. autofunction:: black.standalone_comment_split +.. autofunction:: black.linegen.standalone_comment_split -.. autofunction:: black.transform_line +.. autofunction:: black.linegen.transform_line Caching ------- -.. autofunction:: black.filter_cached +.. autofunction:: black.cache.filter_cached -.. autofunction:: black.get_cache_file +.. autofunction:: black.cache.get_cache_file -.. autofunction:: black.get_cache_info +.. autofunction:: black.cache.get_cache_info -.. autofunction:: black.read_cache +.. autofunction:: black.cache.read_cache -.. autofunction:: black.write_cache +.. autofunction:: black.cache.write_cache Utilities --------- -.. py:function:: black.DebugVisitor.show(code: str) -> None +.. py:function:: black.debug.DebugVisitor.show(code: str) -> None Pretty-print the lib2to3 AST of a given string of `code`. -.. autofunction:: black.cancel +.. autofunction:: black.concurrency.cancel -.. autofunction:: black.child_towards +.. autofunction:: black.nodes.child_towards -.. autofunction:: black.container_of +.. autofunction:: black.nodes.container_of -.. autofunction:: black.convert_one_fmt_off_pair +.. autofunction:: black.comments.convert_one_fmt_off_pair .. autofunction:: black.diff -.. autofunction:: black.dont_increase_indentation +.. autofunction:: black.linegen.dont_increase_indentation -.. autofunction:: black.format_float_or_int_string +.. autofunction:: black.numerics.format_float_or_int_string -.. autofunction:: black.ensure_visible +.. autofunction:: black.nodes.ensure_visible -.. autofunction:: black.enumerate_reversed +.. autofunction:: black.lines.enumerate_reversed -.. autofunction:: black.enumerate_with_length +.. autofunction:: black.comments.generate_comments -.. autofunction:: black.generate_comments +.. autofunction:: black.comments.generate_ignored_nodes -.. autofunction:: black.generate_ignored_nodes +.. autofunction:: black.comments.is_fmt_on -.. autofunction:: black.is_fmt_on +.. autofunction:: black.comments.contains_fmt_on_at_column -.. autofunction:: black.contains_fmt_on_at_column +.. autofunction:: black.nodes.first_leaf_column -.. autofunction:: black.first_leaf_column - -.. autofunction:: black.generate_trailers_to_omit +.. autofunction:: black.linegen.generate_trailers_to_omit .. autofunction:: black.get_future_imports -.. autofunction:: black.list_comments +.. autofunction:: black.comments.list_comments -.. autofunction:: black.make_comment +.. autofunction:: black.comments.make_comment -.. autofunction:: black.maybe_make_parens_invisible_in_atom +.. autofunction:: black.linegen.maybe_make_parens_invisible_in_atom -.. autofunction:: black.max_delimiter_priority_in_atom +.. autofunction:: black.brackets.max_delimiter_priority_in_atom .. autofunction:: black.normalize_fmt_off -.. autofunction:: black.normalize_numeric_literal +.. autofunction:: black.numerics.normalize_numeric_literal -.. autofunction:: black.normalize_prefix +.. autofunction:: black.linegen.normalize_prefix -.. autofunction:: black.normalize_string_prefix +.. autofunction:: black.strings.normalize_string_prefix -.. autofunction:: black.normalize_string_quotes +.. autofunction:: black.strings.normalize_string_quotes -.. autofunction:: black.normalize_invisible_parens +.. autofunction:: black.linegen.normalize_invisible_parens .. autofunction:: black.patch_click -.. autofunction:: black.preceding_leaf +.. autofunction:: black.nodes.preceding_leaf .. autofunction:: black.re_compile_maybe_verbose -.. autofunction:: black.should_split_line +.. autofunction:: black.linegen.should_split_line .. autofunction:: black.shutdown -.. autofunction:: black.sub_twice +.. autofunction:: black.strings.sub_twice -.. autofunction:: black.whitespace +.. autofunction:: black.nodes.whitespace From 5020577788eeab758a2c33d1d563e4ea82617ed4 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 9 May 2021 00:08:03 -0700 Subject: [PATCH 234/680] Remove docker CI from look at 'master' branch (#2209) --- .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 7eaf233ab9c..788c505ee4a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,7 @@ name: docker on: push: branches: - - "master" + - "main" release: types: created From 06ccb88bf2bd35a4dc5d591bb296b5b299d07323 Mon Sep 17 00:00:00 2001 From: Panagiotis Vasilopoulos Date: Sun, 9 May 2021 21:50:17 +0000 Subject: [PATCH 235/680] Replace references to master branch (#2210) Commit history before merge: * Replace references to master branch * Update .flake8 to reference docs on RTD We're moving away from GitHub as a documentation host to only RTD because it's makes our lives easier creating good docs. I know this link is dead right now, but it won't be once we release a new version with the documentation reorganization changes (which should be soon!). Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .flake8 | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 8 ++++---- CHANGES.md | 4 ++++ README.md | 6 +++--- docs/contributing/the_basics.md | 2 +- docs/guides/using_black_with_other_tools.md | 2 +- docs/integrations/editors.md | 2 +- gallery/gallery.py | 8 ++++---- setup.py | 2 +- 9 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.flake8 b/.flake8 index 656c0df24ee..68d15015a37 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] extend-ignore = E203, E266, E501 # line length is intentionally set to 80 here because black uses Bugbear -# See https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-length for more details +# See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3b59906594a..c6c80be5345 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -18,13 +18,13 @@ assignees: "" **Environment (please complete the following information):** -- Version: \[e.g. master\] +- Version: \[e.g. main\] - OS and Python version: \[e.g. Linux/Python 3.7.4rc1\] -**Does this bug also happen on master?** To answer this, you have two options: +**Does this bug also happen on main?** To answer this, you have two options: -1. Use the online formatter at , which will - use the latest master branch. +1. Use the online formatter at , which will use + the latest main branch. 1. Or run _Black_ on your machine: - create a new virtualenv (make sure it's the same Python version); - clone this repository; diff --git a/CHANGES.md b/CHANGES.md index 9b155924b7d..32c2d8d7237 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,10 @@ ### Documentation +- Replaced all remaining references to the + [`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 heading over to [the stable docs on RTD](https://black.readthedocs.io/en/stable/). (#2174) diff --git a/README.md b/README.md index ceccbb79962..beb8069c32b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Black Logo](https://raw.githubusercontent.com/psf/black/master/docs/_static/logo2-readme.png) +![Black Logo](https://raw.githubusercontent.com/psf/black/main/docs/_static/logo2-readme.png)

The Uncompromising Code Formatter

@@ -6,8 +6,8 @@ Actions Status Actions Status Documentation Status -Coverage Status -License: MIT +Coverage Status +License: MIT PyPI Downloads conda-forge diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index 461bff96505..d36b17eff31 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -81,7 +81,7 @@ $ sphinx-build -a -b html -W docs/ docs/_build/ 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/master/src/black_primer/primer.json) +[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 diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 4b22f670fe1..2b3855f7ef0 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -13,7 +13,7 @@ tools out there. tools, using **their** supported file formats. Compatible configuration files can be -[found here](https://github.com/psf/black/blob/master/docs/compatible_configs/). +[found here](https://github.com/psf/black/blob/main/docs/compatible_configs/). ### isort diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 6a69e9e33fd..88176e1aecb 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -166,7 +166,7 @@ automatically installs _Black_. You can upgrade it later by calling `:BlackUpgra restarting Vim. If you need to do anything special to make your virtualenv work and install _Black_ (for -example you want to run a version from master), create a virtualenv manually and point +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`: diff --git a/gallery/gallery.py b/gallery/gallery.py index 6b42ec3a6d4..3df05c1a722 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -74,7 +74,7 @@ def get_top_packages(days: Days) -> List[str]: def get_package_source(package: str, version: Optional[str]) -> str: if package == "cpython": if version is None: - version = "master" + version = "main" return f"https://github.com/python/cpython/archive/{version}.zip" elif package == "pypy": if version is None: @@ -248,9 +248,9 @@ def format_repos(repos: Tuple[Path, ...], options: Namespace) -> None: black_version=black_version, input_directory=options.input, ) - git_switch_branch("master", repo=repo) + git_switch_branch("main", repo=repo) - git_switch_branch("master", repo=options.black_repo) + git_switch_branch("main", repo=options.black_repo) def main() -> None: @@ -296,7 +296,7 @@ def main() -> None: type=Path, help="Output directory to download and put result artifacts.", ) - parser.add_argument("versions", nargs="*", default=("master",), help="") + parser.add_argument("versions", nargs="*", default=("main",), help="") options = parser.parse_args() repos = init_repos(options) diff --git a/setup.py b/setup.py index af93d0f453a..3dc52b26e81 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def get_long_description() -> str: author="Łukasz Langa", author_email="lukasz@langa.pl", url="https://github.com/psf/black", - project_urls={"Changelog": "https://github.com/psf/black/blob/master/CHANGES.md"}, + project_urls={"Changelog": "https://github.com/psf/black/blob/main/CHANGES.md"}, license="MIT", py_modules=["_black_version"], ext_modules=ext_modules, From 3d96b7f10a56fcf826693e98f08b673dad8ac256 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 9 May 2021 22:35:56 -0400 Subject: [PATCH 236/680] Autogenerate black(d|-primer)? help in usage docs (#2212) So these won't go out of date. This does mean the environment has be setup a bit more carefully so the right version of the tool is used, but thankfully the build environment is rebuilt on change on RTD anyway. Also since the HTML docs are known to build fine, let's provide downloadable HTMLzips of our docs. This change needs RTD and GH to install Black with the [d] extra so blackd's help can generated. While editing RTD's config file, let's migrate the file to a non-deprecated filename. Also I missed adding AUTHORS.md to the files key in the doc GHA config. --- .github/workflows/doc.yml | 6 +- readthedocs.yml => .readthedocs.yaml | 9 +- Pipfile | 1 + Pipfile.lock | 90 ++++++++++--------- docs/conf.py | 1 + docs/contributing/gauging_changes.md | 21 +---- docs/requirements.txt | 3 +- .../black_as_a_server.md | 10 +-- docs/usage_and_configuration/the_basics.md | 88 +----------------- 9 files changed, 70 insertions(+), 159 deletions(-) rename readthedocs.yml => .readthedocs.yaml (55%) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 930b6d440ff..74ec316a7cf 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -7,12 +7,14 @@ on: - "README.md" - "CHANGES.md" - "CONTRIBUTING.md" + - "AUTHORS.md" pull_request: paths: - "docs/**" - "README.md" - "CHANGES.md" - "CONTRIBUTING.md" + - "AUTHORS.md" jobs: build: @@ -35,8 +37,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install -e "." + python -m pip install -e ".[d]" python -m pip install -r "docs/requirements.txt" - name: Build documentation - run: sphinx-build -a -b html -W docs/ docs/_build/ + run: sphinx-build -a -b html -W --keep-going docs/ docs/_build diff --git a/readthedocs.yml b/.readthedocs.yaml similarity index 55% rename from readthedocs.yml rename to .readthedocs.yaml index 15065033d0f..24eb3eaf6d9 100644 --- a/readthedocs.yml +++ b/.readthedocs.yaml @@ -1,7 +1,14 @@ version: 2 + +formats: + - htmlzip + python: version: 3.8 install: - requirements: docs/requirements.txt - - method: setuptools + + - method: pip path: . + extra_requirements: + - d diff --git a/Pipfile b/Pipfile index d1842ff749b..b3859571137 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ mypy = ">=0.812" pre-commit = "*" readme_renderer = "*" MyST-Parser = ">=0.13.7" +sphinxcontrib-programoutput = ">=0.17" setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" diff --git a/Pipfile.lock b/Pipfile.lock index f973c8423e8..0b9e529e73b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "62bc4bdb0117234d1f374b2dc0685369f6df7c7192d16409cc9c42a429770166" + "sha256": "fdfbacb362e4514e588736a7e8783837cb5e3aa2fbab98ea17894fdb50d51a8e" }, "pipfile-spec": 6, "requires": {}, @@ -83,11 +83,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "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, @@ -290,13 +290,13 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", "python_version <": "3.8", - "version": "==3.7.4.3", + "version": "==3.10.0.0", "version >=": "3.7.4" }, "yarl": { @@ -420,11 +420,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "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": [ @@ -631,11 +631,11 @@ }, "flake8": { "hashes": [ - "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", - "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", - "version": "==3.9.1" + "version": "==3.9.2" }, "flake8-bugbear": { "hashes": [ @@ -703,11 +703,11 @@ }, "markdown-it-py": { "hashes": [ - "sha256:30b3e9f8198dc82a5df0dcb73fd31d56cd9a43bf8a747feb10b2ba74f962bcb1", - "sha256:c3b9f995be0792cbbc8ab2f53d74072eb7ff8a8b622be8d61d38ab879709eca3" + "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3", + "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389" ], "markers": "python_version ~= '3.6'", - "version": "==0.6.2" + "version": "==1.1.0" }, "markupsafe": { "hashes": [ @@ -776,11 +776,11 @@ }, "mdit-py-plugins": { "hashes": [ - "sha256:1e467ca2ea056e8065cbd5d6c61e5052bb50826bde84c40f6a5ed77e82125710", - "sha256:77fd75dad81109ee91f30eb49146196f79afbbae041f298ae4886c8c2b5e23d7" + "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c", + "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f" ], "markers": "python_version ~= '3.6'", - "version": "==0.2.6" + "version": "==0.2.8" }, "multidict": { "hashes": [ @@ -863,11 +863,11 @@ }, "myst-parser": { "hashes": [ - "sha256:260355b4da8e8865fe080b0638d7f1ab1791dc4bed02a7a48630b6bad4249219", - "sha256:e4bc99e43e19f70d22e528de8e7cce59f7e8e7c4c34dcba203de92de7a7c7c85" + "sha256:8d7db76e2f33cd1dc1fe0c76af9f09e5cf19ce2c2e85074bc82f272c0f7c08ce", + "sha256:fc262959a74cdc799d7fa9b30c320c17187485b9a1e8c39e988fc12f3adff63c" ], "index": "pypi", - "version": "==0.13.7" + "version": "==0.14.0" }, "nodeenv": { "hashes": [ @@ -933,11 +933,11 @@ }, "pygments": { "hashes": [ - "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", - "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" + "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", + "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], "markers": "python_version >= '3.5'", - "version": "==2.8.1" + "version": "==2.9.0" }, "pyparsing": { "hashes": [ @@ -1061,10 +1061,10 @@ }, "rfc3986": { "hashes": [ - "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", - "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", + "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "secretstorage": { "hashes": [ @@ -1084,11 +1084,11 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" + "version": "==1.16.0" }, "snowballstemmer": { "hashes": [ @@ -1137,6 +1137,14 @@ "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", @@ -1215,13 +1223,13 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", "python_version <": "3.8", - "version": "==3.7.4.3", + "version": "==3.10.0.0", "version >=": "3.7.4" }, "urllib3": { @@ -1234,11 +1242,11 @@ }, "virtualenv": { "hashes": [ - "sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2", - "sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535" + "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543", + "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4.4" + "version": "==20.4.6" }, "webencodings": { "hashes": [ diff --git a/docs/conf.py b/docs/conf.py index d5673738452..7162f13dd41 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,6 +59,7 @@ def make_pypi_svg(version: str) -> None: "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "myst_parser", + "sphinxcontrib.programoutput", ] # If you need extensions of a certain version or higher, list them here. diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index 6b70e0bc9bc..b41c7a35dda 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -37,25 +37,6 @@ If you're running locally yourself to test black on lots of code try: ### CLI arguments -```text -Usage: black-primer [OPTIONS] +```{program-output} black-primer --help - primer - prime projects for blackening... 🏴 - -Options: - -c, --config PATH JSON config file path [default: /Users/cooper/repos/ - black/src/black_primer/primer.json] - - --debug Turn on debug logging [default: False] - -k, --keep Keep workdir + repos post run [default: False] - -L, --long-checkouts Pull big projects to test [default: False] - -R, --rebase Rebase project if already checked out [default: - False] - - -w, --workdir PATH Directory path for repo checkouts [default: /var/fol - ders/tc/hbwxh76j1hn6gqjd2n2sjn4j9k1glp/T/primer.20200 - 517125229] - - -W, --workers INTEGER Number of parallel worker coroutines [default: 2] - -h, --help Show this message and exit. ``` diff --git a/docs/requirements.txt b/docs/requirements.txt index 6c34e1fefab..c65cbe3c476 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ -MyST-Parser==0.13.7 +MyST-Parser==0.14.0 Sphinx==3.5.4 +sphinxcontrib-programoutput==0.17 diff --git a/docs/usage_and_configuration/black_as_a_server.md b/docs/usage_and_configuration/black_as_a_server.md index 0c0382b3e50..75a4d925a54 100644 --- a/docs/usage_and_configuration/black_as_a_server.md +++ b/docs/usage_and_configuration/black_as_a_server.md @@ -18,14 +18,8 @@ formatting requests. `blackd` provides even less options than _Black_. You can see them by running `blackd --help`: -```text -Usage: blackd [OPTIONS] - -Options: - --bind-host TEXT Address to bind the server to. - --bind-port INTEGER Port to listen on - --version Show the version and exit. - -h, --help Show this message and exit. +```{program-output} blackd --help + ``` There is no official `blackd` client tool (yet!). You can test that blackd is working diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 6fd8769b591..4ac1693ea8b 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -34,92 +34,8 @@ running `black --help`. Help output -``` - Usage: black [OPTIONS] [SRC]... - - The uncompromising code formatter. - - Options: - -c, --code TEXT Format the code passed in as a string. - -l, --line-length INTEGER How many characters per line to allow. - [default: 88] - - -t, --target-version [py27|py33|py34|py35|py36|py37|py38|py39] - Python versions that should be supported by - Black's output. [default: per-file auto- - detection] - - --pyi Format all input files like typing stubs - regardless of file extension (useful when - piping source on standard input). - - -S, --skip-string-normalization - Don't normalize string quotes or prefixes. - -C, --skip-magic-trailing-comma - Don't use trailing commas as a reason to - split lines. - - --check Don't write the files back, just return the - status. Return code 0 means nothing would - change. Return code 1 means some files - would be reformatted. Return code 123 means - there was an internal error. - - --diff Don't write the files back, just output a - diff for each file on stdout. - - --color / --no-color Show colored diff. Only applies when - `--diff` is given. - - --fast / --safe If --fast given, skip temporary sanity - checks. [default: --safe] - - --include TEXT 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. - [default: \.pyi?$] - - --exclude TEXT 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: /(\.direnv|\.eggs|\.git|\. - hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_bu - ild|buck-out|build|dist)/] - - --extend-exclude TEXT 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 TEXT Like --exclude, but files and directories - matching this regex will be excluded even - when they are passed explicitly as - arguments. - - - --stdin-filename TEXT 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. - - -q, --quiet Don't emit non-error messages to stderr. - Errors are still emitted; silence those with - 2>/dev/null. - - -v, --verbose Also emit messages to stderr about files - that were not changed or were ignored due to - exclusion patterns. - - --version Show the version and exit. - --config FILE Read configuration from FILE path. - -h, --help Show this message and exit. +```{program-output} black --help + ```
From 7c851dfa2c2a8f0bf4c529d7f6faa43798d04325 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 10 May 2021 10:57:22 -0400 Subject: [PATCH 237/680] Cover more in the usage docs (#2208) Commit history before merge: * Cover more in the usage docs * Minor fixes * Even more corrections by Jelle * Update docs/usage_and_configuration/the_basics.md Co-authored-by: Jelle Zijlstra --- docs/usage_and_configuration/the_basics.md | 143 +++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 4ac1693ea8b..0b2cd3b3544 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -40,6 +40,149 @@ running `black --help`. +### Code input alternatives + +#### Standard Input + +_Black_ supports formatting code via stdin, with the result being printed to stdout. +Just let _Black_ know with `-` as the path. + +```console +$ echo "print ( 'hello, world' )" | black - +print("hello, world") +reformatted - +All done! ✨ 🍰 ✨ +1 file reformatted. +``` + +**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. + +#### As a string + +You can also pass code as a string using the `-c` / `--code` option. + +```console +$ black --code "print ( 'hello, world' )" +print("hello, world") +``` + +```{warning} +--check, --diff, and --safe / --fast have no effect when using -c / --code. Safety +checks normally turned on by default that verify _Black_'s output are disabled as well. +This is a bug which we intend to fix eventually. More details can be found in this [bug +report](https://github.com/psf/black/issues/2104). +``` + +### 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. Both variations can be enabled at once. + +#### Exit code + +Passing `--check` will make _Black_ exit with: + +- code 0 if nothing would change; +- code 1 if some files would be reformatted; or +- code 123 if there was an internal error + +```console +$ black test.py --check +All done! ✨ 🍰 ✨ +1 file would be left unchanged. +$ echo $? +0 + +$ black test.py --check +would reformat test.py +Oh no! 💥 💔 💥 +1 file would be reformatted. +$ echo $? +1 + +$ black test.py --check +error: cannot format test.py: INTERNAL ERROR: Black produced code that is not equivalent to the source. Please report a bug on https://github.com/psf/black/issues. This diff might be helpful: /tmp/blk_kjdr1oog.log +Oh no! 💥 💔 💥 +1 file would fail to reformat. +$ echo $? +123 +``` + +#### Diffs + +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`. + +```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 +@@ -1 +1 @@ +-print ( 'hello, world' ) ++print("hello, world") +would reformat test.py +All done! ✨ 🍰 ✨ +1 file would be reformatted. +``` + +### 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 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. +``` + +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. + +```console +$ black src/ -v +Using configuration from /tmp/pyproject.toml. +src/blib2to3 ignored: matches the --extend-exclude regular expression +src/_black_version.py wasn't modified on disk since last run. +src/black/__main__.py wasn't modified on disk since last run. +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 +``` + +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`). + +```console +$ black src/ -q +error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio +``` + +### Getting the version + +You can check the version of _Black_ you have installed using the `--version` flag. + +```console +$ black --version +black, version 21.5b0 +``` + ## Configuration via a file _Black_ is able to read project-specific default values for its command line options From f1ce47bd2b0150d2b7c78beaafd3b60c469345d0 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 10 May 2021 07:58:36 -0700 Subject: [PATCH 238/680] Release process docs (#2214) * Setup groundwork for release process docs I'm using MyST for the index page since I like it more and it's easier to work with. * Fill in Release Process for black * Apply suggestions from code review Apply Jelle's grammar + typo fixes. I am a terrible only English speaker. Co-authored-by: Jelle Zijlstra * Update release_process.md Make lint happy via web UI. * Move to contribution section and fix prettier Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- docs/contributing/index.rst | 2 + docs/contributing/release_process.md | 72 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 docs/contributing/release_process.md diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index 68dfcd60c28..e7a7f9fb231 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -6,6 +6,7 @@ Contributing the_basics gauging_changes + release_process reference/reference_summary Welcome! Happy to see you willing to make the project better. Have you read the entire @@ -31,6 +32,7 @@ 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`. diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md new file mode 100644 index 00000000000..fadef4bbe3d --- /dev/null +++ b/docs/contributing/release_process.md @@ -0,0 +1,72 @@ +# 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 Relase + +To cut a realease, 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 + 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 + 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. + +## 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_ +repository. + +Below are descriptions of our release workflows. + +### Docker + +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™. + +- Currently this workflow uses an API Token associated with @cooperlees account + +### pypi_upload + +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. + +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. + +- Currently this workflow uses an API token associated with @ambv's PyPI account + +### Upload self-contained binaries + +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. + +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 😢). From f933e8a8382e6945bf950356f6ecc4ded5617617 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 10 May 2021 07:59:32 -0700 Subject: [PATCH 239/680] Update CHANGES.md for 21.5b1 release (#2215) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 32c2d8d7237..38eef524b7c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.5b1 ### _Black_ From 2f52e4b4929370ec503ee272bcc10d3176db8e89 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 10 May 2021 08:01:53 -0700 Subject: [PATCH 240/680] fix typo (#2217) --- docs/contributing/release_process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index fadef4bbe3d..ae95cd703c9 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -5,7 +5,7 @@ explain what everything does and how to release _Black_ using said automation. ## Cutting a Relase -To cut a realease, you must be a _Black_ maintainer with `GitHub Release` creation +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 From 53d9bace12b3aa230820c869a079020b4608c945 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 11 May 2021 10:01:03 -0700 Subject: [PATCH 241/680] Add stable tag process to release process documentation (#2224) * Add stable tag process to release process documentation - Add reasoning + step commands * Bah - I ran the linter but forgot to commit * Update docs/contributing/release_process.md Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- docs/contributing/release_process.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index ae95cd703c9..718ea3dc9a2 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -3,7 +3,7 @@ _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 Relase +## 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: @@ -70,3 +70,20 @@ to download the executable for their platform and run _Black_ without a 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 😢). + +## Moving the `stable` tag + +_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. + +### Tag moving process + +#### stable + +From a rebased `main` checkout: + +1. `git tag -f stable VERSION_TAG` + 1. e.g. `git tag -f stable 21.5b1` +1. `git push --tags -f` From 94a0b07dbebabe344991dc27f9eb0b11597bb3b5 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 11 May 2021 14:09:33 -0400 Subject: [PATCH 242/680] Remove useless flake8 config + test support code (#2221) We've depended on Click 7.x ever since we broke CI systems across the world (oops lol) and flake8-mypy was purged a fair bit back: #1867 Also remove the primer tests import in tests/test_black.py because it's annoying when just trying to actually target tests/test_black.py tests. `pytest -k test_black.py` doesn't do what you expect due to that import. --- .flake8 | 4 ---- tests/test_black.py | 35 +++++++---------------------------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/.flake8 b/.flake8 index 68d15015a37..4866133a6a8 100644 --- a/.flake8 +++ b/.flake8 @@ -5,7 +5,3 @@ extend-ignore = E203, E266, E501 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 -# We need to configure the mypy.ini because the flake8-mypy's default -# options don't properly override it, so if we don't specify it we get -# half of the config from mypy.ini and half from flake8-mypy. -mypy_config = mypy.ini diff --git a/tests/test_black.py b/tests/test_black.py index 347eaf621fe..4cbb022954f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from dataclasses import replace import inspect -from io import BytesIO, TextIOWrapper +from io import BytesIO import os from pathlib import Path from platform import system @@ -16,10 +16,8 @@ import types from typing import ( Any, - BinaryIO, Callable, Dict, - Generator, List, Iterator, TypeVar, @@ -52,7 +50,6 @@ ff, dump_to_stderr, ) -from .test_primer import PrimerCLITests # noqa: F401 THIS_FILE = Path(__file__) @@ -104,28 +101,10 @@ def __init__(self) -> None: class BlackRunner(CliRunner): - """Modify CliRunner so that stderr is not merged with stdout. - - This is a hack that can be removed once we depend on Click 7.x""" + """Make sure STDOUT and STDERR are kept seperate when testing Black via its CLI.""" def __init__(self) -> None: - self.stderrbuf = BytesIO() - self.stdoutbuf = BytesIO() - self.stdout_bytes = b"" - self.stderr_bytes = b"" - super().__init__() - - @contextmanager - def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]: - with super().isolation(*args, **kwargs) as output: - try: - hold_stderr = sys.stderr - sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset) - yield output - finally: - self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore - self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore - sys.stderr = hold_stderr + super().__init__(mix_stderr=False) class BlackTestCase(BlackBaseTestCase): @@ -141,8 +120,8 @@ def invokeBlack( exit_code, msg=( f"Failed with args: {args}\n" - f"stdout: {runner.stdout_bytes.decode()!r}\n" - f"stderr: {runner.stderr_bytes.decode()!r}\n" + f"stdout: {result.stdout_bytes.decode()!r}\n" + f"stderr: {result.stderr_bytes.decode()!r}\n" f"exception: {result.exception}" ), ) @@ -483,7 +462,7 @@ def test_python2_should_fail_without_optional_install(self) -> None: finally: os.unlink(tmp_file) actual = ( - runner.stderr_bytes.decode() + result.stderr_bytes.decode() .replace("\n", "") .replace("\\n", "") .replace("\\r", "") @@ -1806,7 +1785,7 @@ def test_preserves_line_endings_via_stdin(self) -> None: black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8")) ) self.assertEqual(result.exit_code, 0) - output = runner.stdout_bytes + output = result.stdout_bytes self.assertIn(nl.encode("utf8"), output) if nl == "\n": self.assertNotIn(b"\r\n", output) From b2ee211b5ad84b62738ac0997b73bf6ee9a74d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 12 May 2021 21:47:32 +0200 Subject: [PATCH 243/680] Click 8.0 renamed its "die on LANG=C" function so we need to look for that one too (#2227) --- CHANGES.md | 6 ++++++ mypy.ini | 6 ++++-- src/black/__init__.py | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 38eef524b7c..becd621dbf2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) + ## 21.5b1 ### _Black_ diff --git a/mypy.ini b/mypy.ini index 589fbf65139..ae7be5e5106 100644 --- a/mypy.ini +++ b/mypy.ini @@ -33,5 +33,7 @@ cache_dir=/dev/null [mypy-aiohttp.*] follow_imports=skip -[mypy-_version] -follow_imports=skip +[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 c61bc8c1d60..f46b866c1bd 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1029,7 +1029,7 @@ def nullcontext() -> Iterator[None]: def patch_click() -> None: - """Make Click not crash. + """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 @@ -1047,7 +1047,9 @@ def patch_click() -> None: for module in (core, _unicodefun): if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None + module._verify_python3_env = lambda: None # type: ignore + if hasattr(module, "_verify_python_env"): + module._verify_python_env = lambda: None # type: ignore def patched_main() -> None: From 3ef339b2e75468a09d617e6aa74bc920c317bce6 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 12 May 2021 21:28:41 -0400 Subject: [PATCH 244/680] Modify when Test, Primer, and Documentation Build run (#2226) - Test and Primer don't run for documentation only changes since it's unnecessary, eating unnecessary cycles and slowing down CI since these workflows eat up the 20 max workers limit quite easily! - Documentation Build runs all of the time now since quite a bit of the content depends on Black's code so even a simple 1-file change in src/black/__init__.py may break the docs build. It's not like this is a costly workflow anyway. Fuzz is still running on all changes because with fuzzing, the more the better in general. 6 or 7 jobs on a documentation only commit is much better than 27/28 jobs anyway :p I also found an error in our bug report issue template :) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/doc.yml | 16 +--------------- .github/workflows/primer.yml | 11 ++++++++++- .github/workflows/test.yml | 11 ++++++++++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c6c80be5345..069795f7776 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ assignees: "" - clone this repository; - run `pip install -e .[d,python2]`; - run `pip install -r test_requirements.txt` - - make sure it's sane by running `python -m unittest`; and + - make sure it's sane by running `python -m pytest`; and - run `black` like you did last time. **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 74ec316a7cf..04b25cf2a16 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,20 +1,6 @@ name: Documentation Build -on: - push: - paths: - - "docs/**" - - "README.md" - - "CHANGES.md" - - "CONTRIBUTING.md" - - "AUTHORS.md" - pull_request: - paths: - - "docs/**" - - "README.md" - - "CHANGES.md" - - "CONTRIBUTING.md" - - "AUTHORS.md" +on: [push, pull_request] jobs: build: diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index 4c5751ae996..5f41c301737 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -1,6 +1,15 @@ name: Primer -on: [push, pull_request] +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + + pull_request: + paths-ignore: + - "docs/**" + - "*.md" jobs: build: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2cfbab67ce1..ef8debb3fb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request] +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + + pull_request: + paths-ignore: + - "docs/**" + - "*.md" jobs: build: From 445f094f1fa2d998bf0cc0007ea48d62953fa876 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 13 May 2021 19:28:41 +0200 Subject: [PATCH 245/680] Use codespell to find typos (#2228) --- CHANGES.md | 1 + docs/index.rst | 2 +- src/black/trans.py | 2 +- src/black_primer/lib.py | 2 +- tests/test_black.py | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index becd621dbf2..3639d8ebaf9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Fix typos discovered by codespell (#2228) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) ## 21.5b1 diff --git a/docs/index.rst b/docs/index.rst index a7a7160f71f..2b85cddd3c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ Try it out now using the `Black Playground `_. 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 versio + 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. diff --git a/src/black/trans.py b/src/black/trans.py index 055f33c0ee2..7ecc31d6d31 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -576,7 +576,7 @@ class StringParenStripper(StringTransformer): - The target string is NOT the only argument to a function call. - The target string is NOT a "pointless" string. - If the target string contains a PERCENT, the brackets are not - preceeded or followed by an operator with higher precedence than + preceded or followed by an operator with higher precedence than PERCENT. Transformations: diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 3ce383f17ce..384c0ad6cea 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -128,7 +128,7 @@ async def black_run( cmd.extend(["--diff", "."]) with TemporaryDirectory() as tmp_path: - # Prevent reading top-level user configs by manipulating envionment variables + # Prevent reading top-level user configs by manipulating environment variables env = { **os.environ, "XDG_CONFIG_HOME": tmp_path, # Unix-like diff --git a/tests/test_black.py b/tests/test_black.py index 4cbb022954f..5ab25cd1601 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -101,7 +101,7 @@ def __init__(self) -> None: class BlackRunner(CliRunner): - """Make sure STDOUT and STDERR are kept seperate when testing Black via its CLI.""" + """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI.""" def __init__(self) -> None: super().__init__(mix_stderr=False) @@ -1508,7 +1508,7 @@ def test_get_sources_with_stdin_filename(self) -> None: @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 mimicing the + # 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" From 904fe94ceba29f3f0c5b8772c4faf699de57338f Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 13 May 2021 15:30:34 -0400 Subject: [PATCH 246/680] Add lower bound for aiohttp-cors + fix primer (#2231) It appears sqlalchemy has recently reformatted their project with Black 21.5b1. Most of our dependencies have a lower bound and creating a test environment with the oldest acceptable dependencies runs the full Black test suite just fine. The only exception to this is aiohttp-cors. It's unbounded and the oldest version 0.1.0 until 0.4.0 breaks the test suite in such an old environment. Failure with 0.1.0: ``` tests/test_blackd.py:10: in import blackd testenv/lib/python3.8/site-packages/blackd/__init__.py:12: in import aiohttp_cors testenv/lib/python3.8/site-packages/aiohttp_cors/__init__.py:29: in from .urldispatcher_router_adapter import UrlDistatcherRouterAdapter testenv/lib/python3.8/site-packages/aiohttp_cors/urldispatcher_router_adapter.py:27: in class UrlDistatcherRouterAdapter(RouterAdapter): testenv/lib/python3.8/site-packages/aiohttp_cors/urldispatcher_router_adapter.py:32: in UrlDistatcherRouterAdapter def route_methods(self, route: web.Route): E AttributeError: module 'aiohttp.web' has no attribute 'Route' ``` For 0.2.0: ``` tests/test_blackd.py:10: in import blackd testenv/lib/python3.8/site-packages/blackd/__init__.py:12: in import aiohttp_cors testenv/lib/python3.8/site-packages/aiohttp_cors/__init__.py:27: in from .cors_config import CorsConfig testenv/lib/python3.8/site-packages/aiohttp_cors/cors_config.py:24: in from .urldispatcher_router_adapter import UrlDistatcherRouterAdapter testenv/lib/python3.8/site-packages/aiohttp_cors/urldispatcher_router_adapter.py:27: in class UrlDistatcherRouterAdapter(AbstractRouterAdapter): testenv/lib/python3.8/site-packages/aiohttp_cors/urldispatcher_router_adapter.py:32: in UrlDistatcherRouterAdapter def route_methods(self, route: web.Route): E AttributeError: module 'aiohttp.web' has no attribute 'Route' ``` For 0.3.0: ``` ERROR: Cannot install aiohttp-cors==0.3.0 and aiohttp==3.6.0 because these package versions have conflicting dependencies. The conflict is caused by: The user requested aiohttp==3.6.0 aiohttp-cors 0.3.0 depends on aiohttp<=0.20.2 and >=0.18.0 To fix this you could try to: 1. loosen the range of package versions you've specified 2. remove package versions to allow pip attempt to solve the dependency conflict ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/user_guide/#fixing-conflicting-dependencies ``` --- CHANGES.md | 5 +++++ setup.py | 2 +- src/black_primer/primer.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3639d8ebaf9..85936e870b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,11 @@ - Fix typos discovered by codespell (#2228) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) +### _Blackd_ + +- Add a lower bound for the `aiohttp-cors` dependency. Only 0.4.0 or higher is + supported. (#2231) + ## 21.5b1 ### _Black_ diff --git a/setup.py b/setup.py index 3dc52b26e81..2b5f71f56dd 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ def get_long_description() -> str: "mypy_extensions>=0.4.3", ], extras_require={ - "d": ["aiohttp>=3.6.0", "aiohttp-cors"], + "d": ["aiohttp>=3.6.0", "aiohttp-cors>=0.4.0"], "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], }, diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 76ed4820487..0bbac85ca2e 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -106,7 +106,7 @@ "sqlalchemy": { "no_cli_args_reason": "breaks black with new string parsing - #2188", "cli_arguments": [], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", "long_checkout": false, "py_versions": ["all"] From 403ce1a18a8a1600ab8249d828e7eaaca442cad7 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 16 May 2021 12:07:27 -0400 Subject: [PATCH 247/680] Add issue triage documentation (#2236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add issue triage documentation Co-authored-by: Łukasz Langa Co-authored-by: Jelle Zijlstra --- Pipfile | 3 +- Pipfile.lock | 120 ++++++++++----------- docs/conf.py | 1 + docs/contributing/index.rst | 1 + docs/contributing/issue_triage.md | 167 ++++++++++++++++++++++++++++++ docs/requirements.txt | 3 + 6 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 docs/contributing/issue_triage.md diff --git a/Pipfile b/Pipfile index b3859571137..789d97c3cbc 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ pre-commit = "*" readme_renderer = "*" MyST-Parser = ">=0.13.7" sphinxcontrib-programoutput = ">=0.17" +sphinx-copybutton = ">=0.3.0" setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" @@ -22,7 +23,7 @@ black = {editable = true, extras = ["d"], path = "."} [packages] aiohttp = ">=3.6.0" -aiohttp-cors = "*" +aiohttp-cors = ">=0.4.0" appdirs = "*" click = ">=7.1.2" mypy_extensions = ">=0.4.3" diff --git a/Pipfile.lock b/Pipfile.lock index 0b9e529e73b..b6dfe438e9e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fdfbacb362e4514e588736a7e8783837cb5e3aa2fbab98ea17894fdb50d51a8e" + "sha256": "b450e698cfe4d856397cb9d0695dcd74eb97b1d234de273179d78c0d0efccd0f" }, "pipfile-spec": 6, "requires": {}, @@ -106,11 +106,11 @@ }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:7d8c289ee437bcb0316820ccee14aefcb056e58d31830ecab8e47eda6540e136", + "sha256:e90e62ced43dc8105fb9a26d62f0d9340b5c8db053a814e25d95c19873ae87db" ], "index": "pypi", - "version": "==7.1.2" + "version": "==8.0.0" }, "dataclasses": { "hashes": [ @@ -516,11 +516,11 @@ }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:7d8c289ee437bcb0316820ccee14aefcb056e58d31830ecab8e47eda6540e136", + "sha256:e90e62ced43dc8105fb9a26d62f0d9340b5c8db053a814e25d95c19873ae87db" ], "index": "pypi", - "version": "==7.1.2" + "version": "==8.0.0" }, "colorama": { "hashes": [ @@ -687,11 +687,11 @@ }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:2f2de5285cf37f33d33ecd4a9080b75c87cd0c1994d5a9c6df17131ea1f049c6", + "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.3" + "markers": "python_version >= '3.6'", + "version": "==3.0.0" }, "keyring": { "hashes": [ @@ -711,61 +711,43 @@ }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95", + "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f", + "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d", + "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc", + "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0", + "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901", + "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66", + "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63", + "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b", + "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5", + "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c", + "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1", + "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05", + "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf", + "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527", + "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb", + "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb", + "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2", + "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730", + "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1", + "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75", + "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b", + "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b", + "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715", + "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b", + "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8", + "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96", + "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348", + "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958", + "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd", + "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6", + "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20", + "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf", + "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.1" + "markers": "python_version >= '3.6'", + "version": "==2.0.0" }, "mccabe": { "hashes": [ @@ -1105,6 +1087,14 @@ "index": "pypi", "version": "==3.5.4" }, + "sphinx-copybutton": { + "hashes": [ + "sha256:0e0461df394515284e3907e3f418a0c60ef6ab6c9a27a800c8552772d0a402a2", + "sha256:5125c718e763596e6e52d92e15ee0d6f4800ad3817939be6dee51218870b3e3d" + ], + "index": "pypi", + "version": "==0.3.1" + }, "sphinxcontrib-applehelp": { "hashes": [ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", diff --git a/docs/conf.py b/docs/conf.py index 7162f13dd41..15adb5df7a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,6 +60,7 @@ def make_pypi_svg(version: str) -> None: "sphinx.ext.napoleon", "myst_parser", "sphinxcontrib.programoutput", + "sphinx_copybutton", ] # If you need extensions of a certain version or higher, list them here. diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index e7a7f9fb231..7e3a90905cd 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -6,6 +6,7 @@ Contributing the_basics gauging_changes + issue_triage release_process reference/reference_summary diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md new file mode 100644 index 00000000000..780b2b5e959 --- /dev/null +++ b/docs/contributing/issue_triage.md @@ -0,0 +1,167 @@ +# Issue triage + +Currently, _Black_ uses the issue tracker for bugs, feature requests, proposed design +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. + +```{tip} +If you're looking for a way to contribute without submitting patches, this might be +the area for you. Since _Black_ is a popular project, its issue tracker is quite busy +and always needs more attention than is available. While triage isn't the most +glamorous or technically challenging form of contribution, it's still important. +For example, we would love to know whether that old bug report is still reproducible! + +You can get easily started by reading over this document and then responding to issues. + +If you contribute enough and have stayed for a long enough time, you may even be +given Triage permissions! +``` + +## The basics + +_Black_ gets a whole bunch of different issues, they range from bug reports to user +support issues. To triage is to identify, organize, and kickstart the issue's journey +through its lifecycle to resolution. + +More specifically, to triage an issue means to: + +- identify what type and categories the issue falls under +- confirm bugs +- ask questions / for further information if necessary +- link related issues +- provide the first initial feedback / support + +Note that triage is typically the first response to an issue, so don't fret if the issue +doesn't make much progress after initial triage. The main goal of triaging to prepare +the issue for future more specific development or discussion, so _eventually_ it will be +resolved. + +The lifecycle of a bug report or user support issue typically goes something like this: + +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`) +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 +5. **awaiting fix** - no further discussion on the issue is necessary and a resolving PR + is the next step +6. **closed** - the issue has been resolved, reasons include: + - the issue couldn't be reproduced + - 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 +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 +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: + - the suggested changes were implemented + - it was rejected (due to technical concerns, ethos conflicts, etc.) + - duplicate of a pre-existing issue or is invalid + +**Note**: documentation issues don't use the `S: accepted` label currently since they're +less likely to be rejected. + +## Labelling + +We use labels to organize, track progress, and help effectively divvy up work. + +Our labels are divided up into several groups identified by their prefix: + +- **T - Type**: the general flavor of issue / PR +- **C - Category**: areas of concerns, ranges from bug types to project maintenance +- **F - Formatting Area**: like C but for formatting specifically +- **S - Status**: what stage of resolution is this issue currently in? +- **R - Resolution**: how / why was the issue / PR resolved? + +We also have a few standalone labels: + +- **`good first issue`**: issues that are beginner-friendly (and will show up in GitHub + banners for first-time visitors to the repository) +- **`help wanted`**: complex issues that need and are looking for a fair bit of work as + to progress (will also show up in various GitHub pages) +- **`skip news`**: for PRs that are trivial and don't need a CHANGELOG entry (and skips + the CHANGELOG entry check) + +```{note} +We do use labels for PRs, in particular the `skip news` label, but we aren't that +rigorous about it. Just follow your judgement on what labels make sense for the +specific PR (if any even make sense). +``` + +## Projects + +For more general and broad goals we use projects to track work. Some may be longterm +projects with no true end (e.g. the "Amazing documentation" project) while others may be +more focused and have a definite end (like the "Getting to beta" project). + +```{note} +To modify GitHub Projects you need the [Write repository permission level or higher](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level). +``` + +## Closing issues + +Closing an issue signifies the issue has reached the end of its life, so closing issues +should be taken with care. The following is the general recommendation for each type of +issue. Note that these are only guidelines and if your judgement says something else +it's totally cool to go with it instead. + +For most issues, closing the issue manually or automatically after a resolving PR is +ideal. For bug reports specifically, if the bug has already been fixed, try to check in +with the issue opener that their specific case has been resolved before closing. + +Design and enhancement issues should be also closed when it's clear the proposed change +won't be implemented, whether that has been determined after a lot of discussion or just +simply goes against _Black_'s ethos. If such an issue turns heated, closing and locking +is acceptable if it's severe enough (although checking in with the core team is probably +a good idea). + +User support issues are best closed by the author or when it's clear the issue has been +resolved in some sort of manner. + +Duplicates and invalid issues should always be closed since they serve no purpose and +add noise to an already busy issue tracker. Although be careful to make sure it's truly +a duplicate and not just very similar before labelling and closing an issue as +duplicate. + +## Common reports + +Some issues are frequently opened, like issues about _Black_ formatted code causing E203 +messages. Even though these issues are probably heavily duplicated, they still require +triage sucking up valuable time from other things (although they usually skip most of +their lifecycle since they're closed on triage). + +Here's some of the most common issues and also pre-made responses you can use: + +### "The trailing comma isn't being removed by Black!" + +```text +Black used to remove the trailing comma if the expression fits in a single line, but this was changed by #826 and #1288. Now a trailing comma tells Black to always explode the expression. This change was made mostly for the cases where you _know_ a collection or whatever will grow in the future. Having it always exploded as one element per line reduces diff noise when adding elements. Before the "magic trailing comma" feature, you couldn't anticipate a collection's growth reliably since collections that fitted in one line were ruthlessly collapsed regardless of your intentions. One of Black's goals is reducing diff noise, so this was a good pragmatic change. + +So no, this is not a bug, but an intended feature. Anyway, [here's the documentation](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#the-magic-trailing-comma) on the "magic trailing comma". Hopefully that helps and sorry for the possible confusion. +``` + +### "Black formatted code is violating Flake8's E203!" + +```text +Hi, + +This is expected behaviour, please see the documentation regarding this case (emphasis +mine): + +> PEP 8 recommends to treat : in slices as a binary operator with the lowest priority, and to leave an equal amount of space on either side, **except if a parameter is omitted (e.g. ham[1 + 1 :])**. It recommends no spaces around : operators for “simple expressions” (ham[lower:upper]), and **extra space for “complex expressions” (ham[lower : upper + offset])**. **Black treats anything more than variable names as “complex” (ham[lower : upper + 1]).** It also states that for extended slices, both : operators have to have the same amount of spacing, except if a parameter is omitted (ham[1 + 1 ::]). Black enforces these rules consistently. + +> This behaviour may raise E203 whitespace before ':' warnings in style guide enforcement tools like Flake8. **Since E203 is not PEP 8 compliant, you should tell Flake8 to ignore these warnings**. + +https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices + +Have a good day! +``` diff --git a/docs/requirements.txt b/docs/requirements.txt index c65cbe3c476..56dbf3ffe13 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,6 @@ +# Used by ReadTheDocs; pinned requirements for stability. + MyST-Parser==0.14.0 Sphinx==3.5.4 sphinxcontrib-programoutput==0.17 +sphinx_copybutton==0.3.1 From 9704922cf92c8f4b26a5efc6ddfdbd42064eae5f Mon Sep 17 00:00:00 2001 From: Matthew Clapp Date: Sun, 16 May 2021 09:10:59 -0700 Subject: [PATCH 248/680] Update vim plugin manual installation instructions. (#2235) --- CHANGES.md | 1 + docs/integrations/editors.md | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 85936e870b1..1a38bf6649b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Fix typos discovered by codespell (#2228) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) +- Fix vim plugin installation instructions. (#2235) ### _Blackd_ diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 88176e1aecb..dd09b4df6d1 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -146,12 +146,15 @@ $ cd ~/.vim/bundle/black $ git checkout origin/stable -b stable ``` -or you can copy the plugin from -[plugin/black.vim](https://github.com/psf/black/blob/stable/plugin/black.vim). +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). ``` mkdir -p ~/.vim/pack/python/start/black/plugin +mkdir -p ~/.vim/pack/python/start/black/autoload curl https://raw.githubusercontent.com/psf/black/stable/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim +curl https://raw.githubusercontent.com/psf/black/stable/autoload/black.vim -o ~/.vim/pack/python/start/black/autoload/black.vim ``` Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or From 60f8bd2c89fbb370e6816fd6d6e5eca64d5b9f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 16 May 2021 18:24:28 +0200 Subject: [PATCH 249/680] Include Jelle's review suggestions --- CHANGES.md | 7 +++++-- docs/contributing/issue_triage.md | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1a38bf6649b..f2c795440f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,15 +4,18 @@ ### _Black_ -- Fix typos discovered by codespell (#2228) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) -- Fix vim plugin installation instructions. (#2235) ### _Blackd_ - Add a lower bound for the `aiohttp-cors` dependency. Only 0.4.0 or higher is supported. (#2231) +### Documentation + +- Fix typos discovered by codespell (#2228) +- Fix Vim plugin installation instructions. (#2235) + ## 21.5b1 ### _Black_ diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 780b2b5e959..9b987fb2425 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -116,7 +116,9 @@ it's totally cool to go with it instead. For most issues, closing the issue manually or automatically after a resolving PR is ideal. For bug reports specifically, if the bug has already been fixed, try to check in -with the issue opener that their specific case has been resolved before closing. +with the issue opener that their specific case has been resolved before closing. Note +that we close issues as soon as they're fixed in the `main` branch. This doesn't +necessarily mean they've been released yet. Design and enhancement issues should be also closed when it's clear the proposed change won't be implemented, whether that has been determined after a lot of discussion or just @@ -146,7 +148,7 @@ Here's some of the most common issues and also pre-made responses you can use: ```text Black used to remove the trailing comma if the expression fits in a single line, but this was changed by #826 and #1288. Now a trailing comma tells Black to always explode the expression. This change was made mostly for the cases where you _know_ a collection or whatever will grow in the future. Having it always exploded as one element per line reduces diff noise when adding elements. Before the "magic trailing comma" feature, you couldn't anticipate a collection's growth reliably since collections that fitted in one line were ruthlessly collapsed regardless of your intentions. One of Black's goals is reducing diff noise, so this was a good pragmatic change. -So no, this is not a bug, but an intended feature. Anyway, [here's the documentation](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#the-magic-trailing-comma) on the "magic trailing comma". Hopefully that helps and sorry for the possible confusion. +So no, this is not a bug, but an intended feature. Anyway, [here's the documentation](https://github.com/psf/black/blob/master/docs/the_black_code_style.md#the-magic-trailing-comma) on the "magic trailing comma", including the ability to skip this functionality with the `--skip-magic-trailing-comma` option. Hopefully that helps solve the possible confusion. ``` ### "Black formatted code is violating Flake8's E203!" From b8450b9faee10f3a0a63378b58adde60a27964e0 Mon Sep 17 00:00:00 2001 From: Hadi Alqattan Date: Sun, 16 May 2021 20:51:27 +0300 Subject: [PATCH 250/680] Fix: black only respects the root gitignore. (#2225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit history before merge: Black now respects .gitignore files in all levels, not only root/.gitignore file (apply .gitignore rules like git does). * Fix: typo * Fix: respect .gitignore files in all levels. * Add: CHANGELOG note. * Fix: TypeError: unsupported operand type(s) for +: 'NoneType' and 'PathSpec' * Update docs. * Fix: no parent .gitignore * Add a comment since the if expression is a bit hard to understand * Update tests - conver no parent .gitignore case. * Use main's Pipfile.lock instead The original changes in Pipfile.lock are whitespace only. The changes turned the JSON's file indentation from 4 to 2. Effectively this happened: `json.dumps(json.loads(old_pipfile_lock), indent=2) + "\n"`. Just using main's Pipfile.lock instead of undoing the changes because 1) I don't know how to do that easily and quickly, and 2) there's a merge conflict. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> * Merge remote-tracking branch 'upstream/main' into i1730 … conflicts for days ay? --- CHANGES.md | 2 ++ .../file_collection_and_discovery.md | 3 +- src/black/files.py | 4 ++- .../nested_gitignore_tests/pyproject.toml | 3 ++ .../nested_gitignore_tests/root/.gitignore | 1 + tests/data/nested_gitignore_tests/root/a.py | 1 + tests/data/nested_gitignore_tests/root/b.py | 1 + tests/data/nested_gitignore_tests/root/c.py | 1 + .../root/child/.gitignore | 1 + .../nested_gitignore_tests/root/child/a.py | 1 + .../nested_gitignore_tests/root/child/b.py | 1 + .../nested_gitignore_tests/root/child/c.py | 1 + tests/data/nested_gitignore_tests/x.py | 0 tests/test_black.py | 29 ++++++++++++++++++- 14 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 tests/data/nested_gitignore_tests/pyproject.toml create mode 100644 tests/data/nested_gitignore_tests/root/.gitignore create mode 100644 tests/data/nested_gitignore_tests/root/a.py create mode 100644 tests/data/nested_gitignore_tests/root/b.py create mode 100644 tests/data/nested_gitignore_tests/root/c.py create mode 100644 tests/data/nested_gitignore_tests/root/child/.gitignore create mode 100644 tests/data/nested_gitignore_tests/root/child/a.py create mode 100644 tests/data/nested_gitignore_tests/root/child/b.py create mode 100644 tests/data/nested_gitignore_tests/root/child/c.py create mode 100644 tests/data/nested_gitignore_tests/x.py diff --git a/CHANGES.md b/CHANGES.md index f2c795440f8..603554cd8b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### _Black_ +- Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply + `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) ### _Blackd_ diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md index 54c76cd9a0f..1f436182dda 100644 --- a/docs/usage_and_configuration/file_collection_and_discovery.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -30,8 +30,7 @@ then write the above files to `.cache/black//`. ## .gitignore If `--exclude` is not set, _Black_ will automatically ignore files and directories in -`.gitignore` file, if present. The `.gitignore` file must be in the project root to be -used and nested `.gitignore` aren't supported. +`.gitignore` file(s), if present. If you want _Black_ to continue using `.gitignore` while also configuring the exclusion rules, please use `--extend-exclude`. diff --git a/src/black/files.py b/src/black/files.py index 1be560643a1..a0c92e8c415 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -204,6 +204,8 @@ def gen_python_files( 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. yield from gen_python_files( child.iterdir(), root, @@ -212,7 +214,7 @@ def gen_python_files( extend_exclude, force_exclude, report, - gitignore, + gitignore + get_gitignore(child) if gitignore is not None else None, ) elif child.is_file(): diff --git a/tests/data/nested_gitignore_tests/pyproject.toml b/tests/data/nested_gitignore_tests/pyproject.toml new file mode 100644 index 00000000000..9ba7ec26980 --- /dev/null +++ b/tests/data/nested_gitignore_tests/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/data/nested_gitignore_tests/root/.gitignore b/tests/data/nested_gitignore_tests/root/.gitignore new file mode 100644 index 00000000000..2987e7bb646 --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/.gitignore @@ -0,0 +1 @@ +a.py diff --git a/tests/data/nested_gitignore_tests/root/a.py b/tests/data/nested_gitignore_tests/root/a.py new file mode 100644 index 00000000000..7135cfd187c --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/a.py @@ -0,0 +1 @@ +# should be excluded (root/.gitignore) diff --git a/tests/data/nested_gitignore_tests/root/b.py b/tests/data/nested_gitignore_tests/root/b.py new file mode 100644 index 00000000000..bdeeca3c602 --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/b.py @@ -0,0 +1 @@ +# should be included diff --git a/tests/data/nested_gitignore_tests/root/c.py b/tests/data/nested_gitignore_tests/root/c.py new file mode 100644 index 00000000000..bdeeca3c602 --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/c.py @@ -0,0 +1 @@ +# should be included diff --git a/tests/data/nested_gitignore_tests/root/child/.gitignore b/tests/data/nested_gitignore_tests/root/child/.gitignore new file mode 100644 index 00000000000..6df81dd798e --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/child/.gitignore @@ -0,0 +1 @@ +b.py diff --git a/tests/data/nested_gitignore_tests/root/child/a.py b/tests/data/nested_gitignore_tests/root/child/a.py new file mode 100644 index 00000000000..7135cfd187c --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/child/a.py @@ -0,0 +1 @@ +# should be excluded (root/.gitignore) diff --git a/tests/data/nested_gitignore_tests/root/child/b.py b/tests/data/nested_gitignore_tests/root/child/b.py new file mode 100644 index 00000000000..c91d47946e6 --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/child/b.py @@ -0,0 +1 @@ +# should be excluded (child/.gitignore) diff --git a/tests/data/nested_gitignore_tests/root/child/c.py b/tests/data/nested_gitignore_tests/root/child/c.py new file mode 100644 index 00000000000..bdeeca3c602 --- /dev/null +++ b/tests/data/nested_gitignore_tests/root/child/c.py @@ -0,0 +1 @@ +# should be included diff --git a/tests/data/nested_gitignore_tests/x.py b/tests/data/nested_gitignore_tests/x.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_black.py b/tests/test_black.py index 5ab25cd1601..098a9ec9157 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1406,7 +1406,7 @@ def test_include_exclude(self) -> None: ) self.assertEqual(sorted(expected), sorted(sources)) - def test_gitingore_used_as_default(self) -> None: + 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/") @@ -1703,6 +1703,33 @@ def test_gitignore_exclude(self) -> None: ) 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, + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + def test_empty_include(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report() From 921c24af8032416101f532e148e6a382d28e28c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Mon, 17 May 2021 21:38:43 +0300 Subject: [PATCH 251/680] Make Prettier preserve line ending type (#2244) Why? The default in Prettier 2.0 was [changed](https://prettier.io/docs/en/options.html#end-of-line) from `auto` to `LF`. This makes development on Windows awkward, because every file is marked with changes both by Prettier and then by Git regardless of repository line ending settings, making committing harder than it should be. --- Aside from that: I noticed that runnin pre-commit manually seems to add line endings to symlink files, but they disappear when actually committing. Don't know if that's a known.. quirk..(?) or not. --- Commit history before merge: * Make Prettier preserve line ending type * Move options to .prettierrc --- .pre-commit-config.yaml | 1 - .prettierrc.yaml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .prettierrc.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e461aa39c9d..2527c4d42ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,4 +28,3 @@ repos: rev: v2.2.1 hooks: - id: prettier - args: [--prose-wrap=always, --print-width=88] diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 00000000000..beda5ba4da8 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,3 @@ +proseWrap: always +printWidth: 88 +endOfLine: auto From 7190d4f6c0dfcc7fce9adfffc1afc7254a01eae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Mon, 17 May 2021 21:47:34 +0300 Subject: [PATCH 252/680] Fix test requirements file name (#2245) --- docs/contributing/the_basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index d36b17eff31..d61f3ec45b6 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -22,7 +22,7 @@ from the cloned _Black_ repo. It will do the correct thing. Non pipenv install works too: ```console -$ pip install -r test_requirements +$ pip install -r test_requirements.txt $ pip install -e .[d] ``` From 3bba8081735925ccd95d8e0ccd26c92e08e0b7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Wed, 19 May 2021 22:11:37 +0300 Subject: [PATCH 253/680] Link isort profile to Black code style isort mention (#2246) The isort configuration currently in the Black code style document is duplicated in Using Black with other tools document. I think it would be better to consolidate information and simply link to the tool guide, mentioning the easy profile in the original document. I changed the link from isort PyPI page to Black's docs on isort because for users it could be better to see the Black docs on why that configuration is necessary and what isort is from Black's perspective. --- docs/the_black_code_style/current_style.md | 26 +++------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 7d08bc9cad5..00001dbc461 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -128,29 +128,9 @@ indentation level (like the arguments list and the docstring in the example abov 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](https://pypi.org/p/isort/) with -the following configuration. - -
-A compatible `.isort.cfg` - -```cfg -[settings] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 -``` - -The equivalent command line is: - -``` -$ isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=88 [ file.py ] -``` - -
+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 From 0b9b7dbdab8c14bb8c33165583e3b540046944e7 Mon Sep 17 00:00:00 2001 From: Salomon Popp Date: Mon, 24 May 2021 03:59:03 +0200 Subject: [PATCH 254/680] Build macOS releases (#2198) * Add macOS release target * Update ubuntu runner Ubuntu 16.04 runner environment is deprecated https://github.blog/changelog/2021-04-29-github-actions-ubuntu-16-04-lts-virtual-environment-will-be-removed-on-september-20-2021/ --- .github/workflows/upload_binary.yml | 18 +++++++++++------- CHANGES.md | 4 ++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 46d92ab2149..39a988752ec 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -11,16 +11,20 @@ jobs: fail-fast: false matrix: python-version: [3.7] - os: [ubuntu-16.04, windows-2019] + os: [windows-2019, ubuntu-20.04, macos-latest] include: - os: windows-2019 pathsep: ";" - executable_suffix: ".exe" + asset_name: black_windows.exe executable_mime: "application/vnd.microsoft.portable-executable" - - os: ubuntu-16.04 + - os: ubuntu-20.04 pathsep: ":" - executable_suffix: ".elf" + asset_name: black_linux executable_mime: "application/x-executable" + - os: macos-latest + pathsep: ":" + asset_name: black_macos + executable_mime: "application/x-mach-binary" steps: - uses: actions/checkout@v2 @@ -38,7 +42,7 @@ jobs: - name: Build binary run: | - python -m PyInstaller -F --name black${{ matrix.executable_suffix }} --add-data 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py + 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 @@ -46,6 +50,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: dist/black${{ matrix.executable_suffix }} - asset_name: black${{ matrix.executable_suffix }} + asset_path: dist/${{ matrix.asset_name }} + asset_name: ${{ matrix.asset_name }} asset_content_type: ${{ matrix.executable_mime }} diff --git a/CHANGES.md b/CHANGES.md index 603554cd8b7..f795c797bf5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,10 @@ - Add a lower bound for the `aiohttp-cors` dependency. Only 0.4.0 or higher is supported. (#2231) +### _Packaging_ + +- Release self-contained macOS binaries as part of the GitHub release pipeline (#2198) + ### Documentation - Fix typos discovered by codespell (#2228) From 3759b856af3434e96ff48ac635928079a4a48ae7 Mon Sep 17 00:00:00 2001 From: temeddix <66480156+temeddix@users.noreply.github.com> Date: Mon, 24 May 2021 11:19:03 +0900 Subject: [PATCH 255/680] Solved Problem with Non-ASCII .gitignore Files (#2229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Solved Problem with non-alphabetical .gitignore files When .gitignore file in the user's project directory contained non-alphabetical characters(Japanese, Korean, Chinese, etc), Nothing works and printed this weird message in the console('cp949' is the encoding for Korean characters in this case). It even blocks VSCode's formatting from working. This commit solves the problem. Traceback (most recent call last): File "c:\users\username\anaconda3\envs\project-name\lib\runpy.py", line 193, in _run_module_as_main "__main__", mod_spec) File "c:\users\username\anaconda3\envs\project-name\lib\runpy.py", line 85, in _run_code exec(code, run_globals) File "C:\Users\username\anaconda3\envs\project-name\Scripts\black.exe\__main__.py", line 7, in File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\black\__init__.py", line 1056, in patched_main main() File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\click\core.py", line 829, in __call__ return self.main(*args, **kwargs) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\click\core.py", line 782, in main rv = self.invoke(ctx) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\click\core.py", line 1066, in invoke return ctx.invoke(self.callback, **ctx.params) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\click\core.py", line 610, in invoke return callback(*args, **kwargs) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\click\decorators.py", line 21, in new_func return f(get_current_context(), *args, **kwargs) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\black\__init__.py", line 394, in main stdin_filename=stdin_filename, File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\black\__init__.py", line 445, in get_sources gitignore = get_gitignore(root) File "c:\users\username\anaconda3\envs\project-name\lib\site-packages\black\files.py", line 122, in get_gitignore lines = gf.readlines() UnicodeDecodeError: 'cp949' codec can't decode byte 0xb0 in position 13: illegal multibyte sequence * Made .gitignore File Reader Detect Its Encoding * Revert "Made .gitignore File Reader Detect Its Encoding" This reverts commit 6c3a7ea42b5b1e441cc0026c8205d1cee68c1bba. * Revert "Solved Problem with non-alphabetical .gitignore files" This reverts commit b0100b5d91c2f5db544a60f34aafab120f0aa458. * Made .gitignore Reader Open the File with Auto Encoding Detecting https://docs.python.org/3.8/library/tokenize.html#tokenize.open * Revert "Made .gitignore Reader Open the File with Auto Encoding Detecting" This reverts commit 50dd80422938649ccc8c7f43aac752f9f6481779. * Made .gitignore Reader Use UTF-8 * Updated CHANGES.md for #2229 * Updated CHANGES.md for #2229 * Update CHANGES.md * Update CHANGES.md Co-authored-by: Jelle Zijlstra Co-authored-by: Łukasz Langa Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 1 + src/black/files.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f795c797bf5..34fa1a2092a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Fix handling of .gitignore files containing non-ASCII characters on Windows (#2229) - Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) diff --git a/src/black/files.py b/src/black/files.py index a0c92e8c415..de516156605 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -118,7 +118,7 @@ def get_gitignore(root: Path) -> PathSpec: gitignore = root / ".gitignore" lines: List[str] = [] if gitignore.is_file(): - with gitignore.open() as gf: + with gitignore.open(encoding="utf-8") as gf: lines = gf.readlines() return PathSpec.from_lines("gitwildmatch", lines) From 04518c38c945ecf3c15765b71642112495cade8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Tue, 25 May 2021 23:07:05 +0300 Subject: [PATCH 256/680] Create FAQ documentation (GH-2247) This commit creates a Frequently Asked Questions document for our users to read. Hopefully they actually read it too. Items included are: Black's non-API, AST safety, style stability, file discovery, Flake8 disagreements and Python 2 support. Hopefully I've got the answers down in general. Commit history before merge: * Create FAQ * Address feedback * Move to single markdown file * Minor wording improvements * Add changelog entry --- CHANGES.md | 1 + docs/_static/custom.css | 6 +++++ docs/faq.md | 52 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 4 files changed, 60 insertions(+) create mode 100644 docs/faq.md diff --git a/CHANGES.md b/CHANGES.md index 34fa1a2092a..22939e38c12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) +- Add new Frequently Asked Questions page (#2247) ## 21.5b1 diff --git a/docs/_static/custom.css b/docs/_static/custom.css index c06c40a2dfc..eacd69c15a0 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -36,3 +36,9 @@ _:-ms-fullscreen, -ms-overflow-style: none; } } + +/* Nicer style for local document toc */ +.contents.topic { + background: none; + border: none; +} diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000000..46e459883ed --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,52 @@ +# Frequently Asked Questions + +The most common questions and issues users face are aggregated to this FAQ. + +```{contents} +:local: +:backlinks: none +``` + +## 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 +of them. A simple API is being [planned](https://github.com/psf/black/issues/779) +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 +[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. + +## 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. + +## Why is my file not formatted? + +Most likely because it is ignored in `.gitignore` or excluded with configuration. See +[file collection and discovery](usage_and_configuration/file_collection_and_discovery.md) +for details. + +## Why are Flake8's E203 and W503 violated? + +Because they go against PEP 8. E203 falsely triggers on list +[slices](the_black_code_style/current_style.md#slices), and adhering to W503 hinders +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? + +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. diff --git a/docs/index.rst b/docs/index.rst index 2b85cddd3c0..53857995ec6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -97,6 +97,7 @@ Contents usage_and_configuration/index integrations/index guides/index + faq .. toctree:: :maxdepth: 3 From 92f20d7f8493f9d0f27531f68b776fb435d53d2e Mon Sep 17 00:00:00 2001 From: Mark Bell Date: Tue, 25 May 2021 23:43:28 +0100 Subject: [PATCH 257/680] Removed adding a space into empty docstrings. (#2249) Resolves #2168 by disabling the insertion of a " " when the docstring is entirely empty. Note that this PR is focussed only on the case of empty docstrings. In particular this does not make any changes to the behaviour that a " " is inserted if a non-empty docstring begins with the quoting character. That is, black still prefers: """ "something" """ to: """"something" """ and that: """"Something"""" is not a legal docstring. --- CHANGES.md | 1 + src/black/linegen.py | 4 ++-- tests/data/docstring.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 22939e38c12..1b7a54a516d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- A space is no longer inserted into empty docstrings (#2249) - Fix handling of .gitignore files containing non-ASCII characters on Windows (#2229) - Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply `.gitignore` rules like `git` does) (#2225) diff --git a/src/black/linegen.py b/src/black/linegen.py index 2e16b6fde44..7949654b40e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -236,6 +236,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: # characters but only if they are the same as the first. quote_len = 1 if docstring[1] != quote_char else 3 docstring = docstring[quote_len:-quote_len] + docstring_started_empty = not docstring if is_multiline_string(leaf): indent = " " * 4 * self.current_line.depth @@ -255,8 +256,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 += " " - else: - # Add some padding if the docstring is empty. + elif not docstring_started_empty: docstring = " " # We could enforce triple quotes at this point. diff --git a/tests/data/docstring.py b/tests/data/docstring.py index e977619e482..96bcf525b16 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -102,7 +102,7 @@ def and_this(): "hey yah"''' -def empty(): +def multiline_whitespace(): ''' @@ -111,11 +111,11 @@ def empty(): ''' -def oneline_empty(): +def oneline_whitespace(): ''' ''' -def oneline_nothing(): +def empty(): """""" @@ -293,16 +293,16 @@ def and_this(): "hey yah"''' -def empty(): +def multiline_whitespace(): """ """ -def oneline_empty(): +def oneline_whitespace(): """ """ -def oneline_nothing(): - """ """ +def empty(): + """""" def single_quotes(): @@ -374,4 +374,4 @@ 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" \ No newline at end of file + "I'm sorry Dave" From 754eecf69eed0bdc75fe224c19a702a4f0676807 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Wed, 26 May 2021 05:52:09 -0700 Subject: [PATCH 258/680] Add optional uvloop import (#2258) * Add optional uvloop import - If we find `uvloop` in the env for black, blackd or black-primer lets try and use it - Add a uvloop extra install Fixes #2257 Test: - Add ci job to install black[uvloop] and run a primer run with uvloop - Only with latest python (3.9) - Will be handy to compare runtimes as a very unoffical benchmark * Remove tox install * Add to CHANGES/news --- .github/workflows/uvloop_test.yml | 45 +++++++++++++++++++++++++++++++ CHANGES.md | 1 + setup.py | 1 + src/black/__init__.py | 7 +++++ src/black_primer/cli.py | 8 ++++++ src/blackd/__init__.py | 8 ++++++ 6 files changed, 70 insertions(+) create mode 100644 .github/workflows/uvloop_test.yml diff --git a/.github/workflows/uvloop_test.yml b/.github/workflows/uvloop_test.yml new file mode 100644 index 00000000000..5d23ec64299 --- /dev/null +++ b/.github/workflows/uvloop_test.yml @@ -0,0 +1,45 @@ +name: test uvloop + +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: + os: [ubuntu-latest, macOS-latest] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + + - name: Install latest pip + run: | + python -m pip install --upgrade pip + + - name: Test uvloop Extra Install + run: | + python -m pip install -e ".[uvloop]" + + - name: Primer uvloop run + run: | + black-primer diff --git a/CHANGES.md b/CHANGES.md index 1b7a54a516d..de326e4ee7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ - Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) +- Add extra uvloop install + import support if in python env (#2258) ### _Blackd_ diff --git a/setup.py b/setup.py index 2b5f71f56dd..5549ae35342 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ def get_long_description() -> str: "d": ["aiohttp>=3.6.0", "aiohttp-cors>=0.4.0"], "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], + "uvloop": ["uvloop>=0.15.2"], }, test_suite="tests.test_black", classifiers=[ diff --git a/src/black/__init__.py b/src/black/__init__.py index f46b866c1bd..90aad220a9c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -54,6 +54,13 @@ from _black_version import version as __version__ +# If our environment has uvloop installed lets use it +try: + import uvloop + + uvloop.install() +except ImportError: + pass # types FileContent = str diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index 00b54d6511f..f0997049d21 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -13,6 +13,14 @@ 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") diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index fc684730e4b..10b616894da 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -22,6 +22,14 @@ import black import click +# If our environment has uvloop installed lets use it +try: + import uvloop + + uvloop.install() +except ImportError: + pass + from _black_version import version as __version__ # This is used internally by tests to shut down the server prematurely From 6613e76658143bcbbe363da76461e0202d589400 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 26 May 2021 22:04:10 -0400 Subject: [PATCH 259/680] Fix and test docs on Windows (#2262) There's some weird interaction between Click and sphinxcontrib-programoutput on Windows that leads to an encoding error during the printing of black-primer's help text. Also symlinks aren't well supported on Windows so let's just use includes which actually work because we now use MyST :D --- .github/workflows/doc.yml | 11 +++++++---- CHANGES.md | 1 + docs/authors.md | 4 +++- docs/change_log.md | 4 +++- docs/conf.py | 8 +++++++- 5 files changed, 21 insertions(+), 7 deletions(-) mode change 120000 => 100644 docs/authors.md mode change 120000 => 100644 docs/change_log.md diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 04b25cf2a16..5689d2887c4 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -11,14 +11,17 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up latest Python uses: actions/setup-python@v2 - with: - python-version: 3.9 - name: Install dependencies run: | diff --git a/CHANGES.md b/CHANGES.md index de326e4ee7b..0a87fd1dbf4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) - Add new Frequently Asked Questions page (#2247) +- Fix encoding + symlink issues preventing proper build on Windows (#2262) ## 21.5b1 diff --git a/docs/authors.md b/docs/authors.md deleted file mode 120000 index 3234d6e0792..00000000000 --- a/docs/authors.md +++ /dev/null @@ -1 +0,0 @@ -../AUTHORS.md \ No newline at end of file diff --git a/docs/authors.md b/docs/authors.md new file mode 100644 index 00000000000..21b0e1a1f5b --- /dev/null +++ b/docs/authors.md @@ -0,0 +1,3 @@ +```{include} ../AUTHORS.md + +``` diff --git a/docs/change_log.md b/docs/change_log.md deleted file mode 120000 index cf547089dc1..00000000000 --- a/docs/change_log.md +++ /dev/null @@ -1 +0,0 @@ -../CHANGES.md \ No newline at end of file diff --git a/docs/change_log.md b/docs/change_log.md new file mode 100644 index 00000000000..e5f67e755d3 --- /dev/null +++ b/docs/change_log.md @@ -0,0 +1,3 @@ +```{include} ../CHANGES.md + +``` diff --git a/docs/conf.py b/docs/conf.py index 15adb5df7a1..55d0fa99dc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,8 +12,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -from pathlib import Path + +import os import string +from pathlib import Path from pkg_resources import get_distribution @@ -29,6 +31,10 @@ def make_pypi_svg(version: str) -> None: f.write(svg) +# Necessary so Click doesn't hit an encode error when called by +# sphinxcontrib-programoutput on Windows. +os.putenv("pythonioencoding", "utf-8") + # -- Project information ----------------------------------------------------- project = "Black" From d3670d9c6568aef270ccda3006252ee3388cf587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Thu, 27 May 2021 16:58:06 +0300 Subject: [PATCH 260/680] Use latest Python in uploading binaries (#2260) * Use latest Python in uploading binaries * Don't pin version at all * Add changelog entry --- .github/workflows/upload_binary.yml | 5 ++--- CHANGES.md | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index 39a988752ec..766f37cc321 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -10,7 +10,6 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7] os: [windows-2019, ubuntu-20.04, macos-latest] include: - os: windows-2019 @@ -29,10 +28,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up latest Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} + python-version: "*" - name: Install dependencies run: | diff --git a/CHANGES.md b/CHANGES.md index 0a87fd1dbf4..5f570e2b8c7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ ### _Packaging_ - Release self-contained macOS binaries as part of the GitHub release pipeline (#2198) +- Always build binaries with the latest available Python (#2260) ### Documentation From 7f138c11306cdf4be360d018528c33a3e024dd6b Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 29 May 2021 15:56:46 +0200 Subject: [PATCH 261/680] Issue templates: use HTML comments (#2269) This commit makes use of HTML comments inside GitHub issue templates to make sure that even if they aren't removed by the issue author they won't be shown in the rendered output. The goal is to simply make the issues less noisy by removing template messages. --- .github/ISSUE_TEMPLATE/bug_report.md | 31 +++++++++++++++-------- .github/ISSUE_TEMPLATE/feature_request.md | 24 ++++++++++++------ .github/ISSUE_TEMPLATE/style_issue.md | 20 ++++++++++----- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 069795f7776..9924408b823 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,22 +6,31 @@ labels: bug assignees: "" --- -**Describe the bug** A clear and concise description of what the bug is. +**Describe the bug** -**To Reproduce** Steps to reproduce the behavior: + +**To Reproduce** + + + +**Expected behavior** -**Expected behavior** A clear and concise description of what you expected to happen. + **Environment (please complete the following information):** -- Version: \[e.g. main\] -- OS and Python version: \[e.g. Linux/Python 3.7.4rc1\] +- Version: +- OS and Python version: -**Does this bug also happen on main?** To answer this, you have two options: +**Does this bug also happen on main?** + + + +**Additional context** -**Additional context** Add any other context about the problem here. + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 56c2f0d0185..7c8cd1c1a07 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,14 +6,22 @@ labels: enhancement assignees: "" --- -**Is your feature request related to a problem? Please describe.** A clear and concise -description of what the problem is. Ex. I'm always frustrated when \[...\] +**Is your feature request related to a problem? Please describe.** -**Describe the solution you'd like** A clear and concise description of what you want to -happen. + -**Describe alternatives you've considered** A clear and concise description of any -alternative solutions or features you've considered. +**Describe the solution you'd like** -**Additional context** Add any other context or screenshots about the feature request -here. + + +**Describe alternatives you've considered** + + + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/style_issue.md b/.github/ISSUE_TEMPLATE/style_issue.md index 6d1f246ed86..2ffd102712d 100644 --- a/.github/ISSUE_TEMPLATE/style_issue.md +++ b/.github/ISSUE_TEMPLATE/style_issue.md @@ -6,11 +6,15 @@ labels: design assignees: "" --- -**Describe the style change** A clear and concise description of how the style can be -improved. +**Describe the style change** -**Examples in the current _Black_ style** Think of some short code snippets that show -how the current _Black_ style is not great: + + +**Examples in the current _Black_ style** + + ```python def f(): @@ -18,7 +22,9 @@ def f(): pass ``` -**Desired style** How do you think _Black_ should format the above snippets: +**Desired style** + + ```python def f( @@ -26,4 +32,6 @@ def f( pass ``` -**Additional context** Add any other context about the problem here. +**Additional context** + + From 33e2b4401439f2015065704c08d22f008f016235 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 May 2021 07:27:54 -0700 Subject: [PATCH 262/680] Add --experimental-string-processing to future changes (#2273) * add esp to future style * changelog * fix label --- CHANGES.md | 1 + docs/the_black_code_style/current_style.md | 2 ++ docs/the_black_code_style/future_style.md | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5f570e2b8c7..0d595bb0d50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ ### Documentation +- `--experimental-string-processing` will be enabled by default in the future (#2273) - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) - Add new Frequently Asked Questions page (#2247) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 00001dbc461..8c3a30270d1 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -250,6 +250,8 @@ 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 diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index aca9fe04017..a7676090553 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -33,3 +33,10 @@ 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). From ab9baf0d65dedb87a853f87afc63d40276e10b3b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 May 2021 09:03:08 -0700 Subject: [PATCH 263/680] Fix path_empty() (#2275) Behavior other than output shouldn't depend on the verbose/quiet option. As far as I can tell this currently has no visible effect, since code after this function is called handles an empty list gracefully. --- src/black/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 90aad220a9c..d83f0e54a72 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -506,8 +506,9 @@ def path_empty( """ Exit if there is no `src` provided for formatting """ - if not src and (verbose or not quiet): - out(msg) + if not src: + if verbose or not quiet: + out(msg) ctx.exit(0) From 898815bc833779bd6c6f3e67710644f715b07316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 29 May 2021 18:04:45 +0200 Subject: [PATCH 264/680] Add @zzzeek testimonial to README and docs --- README.md | 7 +++++++ docs/index.rst | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index beb8069c32b..7b4aeee8baa 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,13 @@ Are we missing anyone? Let us know. ## 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): diff --git a/docs/index.rst b/docs/index.rst index 53857995ec6..44b800cadf8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,13 @@ Try it out now using the `Black Playground `_. 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.* From 009a17739d64b86e10cc6aac35477cadf51cdcfb Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 29 May 2021 10:15:22 -0700 Subject: [PATCH 265/680] ptr nolong requires changes (#2276) - I worked on this project yesterday and must have fixed the formatting --- 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 0bbac85ca2e..034372609a1 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -91,7 +91,7 @@ }, "ptr": { "cli_arguments": ["--experimental-string-processing"], - "expect_formatting_changes": true, + "expect_formatting_changes": false, "git_clone_url": "https://github.com/facebookincubator/ptr.git", "long_checkout": false, "py_versions": ["all"] From 519f807f879350704449e424c5c5b7e049e582b3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 May 2021 10:16:33 -0700 Subject: [PATCH 266/680] add discussion of magic comments to FAQ (#2272) Co-authored-by: Cooper Lees --- CHANGES.md | 1 + docs/faq.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0d595bb0d50..d9800a6979c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ ### Documentation +- Add discussion of magic comments to FAQ page (#2272) - `--experimental-string-processing` will be enabled by default in the future (#2273) - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) diff --git a/docs/faq.md b/docs/faq.md index 46e459883ed..ac5ba937c1c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -21,7 +21,8 @@ _Black_ is still in [beta](index.rst), some edges are still a bit rough. To comb issues, the equivalence of code after formatting 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. +is raised and the file is left untouched. Magical comments that influence linters and +other tools, such as `# noqa`, may be moved by _Black_. See below for more details. ## How stable is Black's style? @@ -50,3 +51,11 @@ For formatting, yes! [Install](getting_started.md#installation) with the `python 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. + +## 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 +influence their behavior. While Black does its best to recognize such comments and leave +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_. From eec44f5977f195a10b81676525f463d0b634bd80 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 30 May 2021 15:32:28 -0400 Subject: [PATCH 267/680] Fix --experiemental-string-processing crash when matching parens not found (#2283) Fixes #2271 --- CHANGES.md | 1 + src/black/trans.py | 14 ++++++++++---- tests/data/long_strings__regression.py | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d9800a6979c..63c7a2cf30d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) - Add extra uvloop install + import support if in python env (#2258) +- Fix --experimental-string-processing crash when matching parens are not found (#2283) ### _Blackd_ diff --git a/src/black/trans.py b/src/black/trans.py index 7ecc31d6d31..169b675be3d 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -25,7 +25,7 @@ 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 CLOSING_BRACKETS, STANDALONE_COMMENT +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 @@ -1398,6 +1398,11 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): def do_splitter_match(self, line: Line) -> TMatchResult: LL = line.leaves + if line.leaves[-1].type in OPENING_BRACKETS: + return TErr( + "Cannot wrap parens around a line that ends in an opening bracket." + ) + string_idx = ( self._return_match(LL) or self._else_match(LL) @@ -1665,9 +1670,10 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: right_leaves.pop() if old_parens_exist: - assert ( - right_leaves and right_leaves[-1].type == token.RPAR - ), "Apparently, old parentheses do NOT exist?!" + assert right_leaves and right_leaves[-1].type == token.RPAR, ( + "Apparently, old parentheses do NOT exist?!" + f" (left_leaves={left_leaves}, right_leaves={right_leaves})" + ) old_rpar_leaf = right_leaves.pop() append_leaves(string_line, line, right_leaves) diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 2e7f2483b63..231d88651b6 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -396,6 +396,16 @@ def xxxxxxx_xxxxxx(xxxx): " it has now" ) + +def _legacy_listen_examples(): + text += ( + " \"listen for the '%(event_name)s' event\"\n" + "\n # ... (event logic logic logic) ...\n" + % { + "since": since, + } + ) + # output @@ -886,3 +896,13 @@ def xxxxxxx_xxxxxx(xxxx): " it goes over 88 characters which" " it has now" ) + + +def _legacy_listen_examples(): + text += ( + " \"listen for the '%(event_name)s' event\"\n" + "\n # ... (event logic logic logic) ...\n" + % { + "since": since, + } + ) From 4ca4407b4adc49b96c9536b16ed7d0a1e0b2deca Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 30 May 2021 17:41:03 -0400 Subject: [PATCH 268/680] Make sure to split lines that start with a string operator (#2286) Fixes #2284 --- CHANGES.md | 1 + src/black/trans.py | 78 +++++++++++++++++------- tests/data/long_strings__regression.py | 84 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 63c7a2cf30d..c761d14624a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) - Add extra uvloop install + import support if in python env (#2258) - Fix --experimental-string-processing crash when matching parens are not found (#2283) +- Make sure to split lines that start with a string operator (#2286) ### _Blackd_ diff --git a/src/black/trans.py b/src/black/trans.py index 169b675be3d..80e88a2d2fb 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -920,9 +920,9 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): lines by themselves). Requirements: - * The line consists ONLY of a single string (with the exception of a - '+' symbol which MAY exist at the start of the line), MAYBE a string - trailer, and MAYBE a trailing comma. + * 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 * All of the requirements listed in BaseStringSplitter's docstring. @@ -952,6 +952,16 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): CustomSplit objects and add them to the custom split map. """ + STRING_OPERATORS = [ + token.PLUS, + token.STAR, + token.EQEQUAL, + token.NOTEQUAL, + token.LESS, + token.LESSEQUAL, + token.GREATER, + token.GREATEREQUAL, + ] MIN_SUBSTR_SIZE = 6 # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. RE_FEXPR = r""" @@ -972,8 +982,20 @@ def do_splitter_match(self, line: Line) -> TMatchResult: idx = 0 - # The first leaf MAY be a '+' symbol... - if is_valid_index(idx) and LL[idx].type == token.PLUS: + # The first two leaves MAY be the 'not in' keywords... + if ( + is_valid_index(idx) + and is_valid_index(idx + 1) + and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME] + and str(LL[idx]) + str(LL[idx + 1]) == "not in" + ): + idx += 2 + # Else the first leaf MAY be a string operator symbol or the 'in' keyword... + elif is_valid_index(idx) and ( + LL[idx].type in self.STRING_OPERATORS + or LL[idx].type == token.NAME + and str(LL[idx]) == "in" + ): idx += 1 # The next/first leaf MAY be an empty LPAR... @@ -1023,23 +1045,26 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: ) first_string_line = True - starts_with_plus = LL[0].type == token.PLUS - def line_needs_plus() -> bool: - return first_string_line and starts_with_plus + 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 + if string_op_leaves + else 0 + ) - def maybe_append_plus(new_line: Line) -> None: + def maybe_append_string_operators(new_line: Line) -> None: """ Side Effects: - If @line starts with a plus and this is the first line we are - constructing, this function appends a PLUS leaf to @new_line - and replaces the old PLUS leaf in the node structure. Otherwise - this function does nothing. + If @line starts with a string operator and this is the first + line we are constructing, this function appends the string + operator to @new_line and replaces the old string operator leaf + in the node structure. Otherwise this function does nothing. """ - if line_needs_plus(): - plus_leaf = Leaf(token.PLUS, "+") - replace_child(LL[0], plus_leaf) - new_line.append(plus_leaf) + maybe_prefix_leaves = string_op_leaves if first_string_line else [] + for i, prefix_leaf in enumerate(maybe_prefix_leaves): + replace_child(LL[i], prefix_leaf) + new_line.append(prefix_leaf) ends_with_comma = ( is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA @@ -1054,7 +1079,7 @@ def max_last_string() -> int: result = self.line_length result -= line.depth * 4 result -= 1 if ends_with_comma else 0 - result -= 2 if line_needs_plus() else 0 + result -= string_op_leaves_length return result # --- Calculate Max Break Index (for string value) @@ -1103,7 +1128,7 @@ def more_splits_should_be_made() -> bool: break_idx = csplit.break_idx else: # Algorithmic Split (automatic) - max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx + max_bidx = max_break_idx - 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 @@ -1148,7 +1173,7 @@ def more_splits_should_be_made() -> bool: # --- Construct `next_line` next_line = line.clone() - maybe_append_plus(next_line) + maybe_append_string_operators(next_line) next_line.append(next_leaf) string_line_results.append(Ok(next_line)) @@ -1169,7 +1194,7 @@ def more_splits_should_be_made() -> bool: self._maybe_normalize_string_quotes(rest_leaf) last_line = line.clone() - maybe_append_plus(last_line) + maybe_append_string_operators(last_line) # If there are any leaves to the right of the target string... if is_valid_index(string_idx + 1): @@ -1345,6 +1370,17 @@ def _normalize_f_string(self, string: str, prefix: str) -> str: else: return string + def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]: + LL = list(leaves) + + string_op_leaves = [] + i = 0 + while LL[i].type in self.STRING_OPERATORS + [token.NAME]: + prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip()) + string_op_leaves.append(prefix_leaf) + i += 1 + return string_op_leaves + class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter): """ diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 231d88651b6..bd7b6351321 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -406,6 +406,40 @@ def _legacy_listen_examples(): } ) + +assert str(suffix_arr) == ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) != ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) <= ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) >= ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) < ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) > ( + "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert str(suffix_arr) in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +assert str(suffix_arr) not in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" + # output @@ -906,3 +940,53 @@ def _legacy_listen_examples(): "since": since, } ) + + +assert ( + str(suffix_arr) + == "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + != "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + <= "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + >= "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + < "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + > "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " + "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " + "'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +) +assert ( + str(suffix_arr) + in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$'," + " 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$', 'rykangaroo$'," + " 'ykangaroo$']" +) +assert ( + str(suffix_arr) + not in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$'," + " 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$'," + " 'rykangaroo$', 'ykangaroo$']" +) From 199e3eb76b74d9d2cada527403a3989287c4e8b3 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 30 May 2021 18:34:33 -0400 Subject: [PATCH 269/680] Fix regular expression that black uses to identify f-expressions (#2287) Fixes #1469 --- CHANGES.md | 1 + src/black/trans.py | 4 ++-- tests/data/long_strings__regression.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c761d14624a..39256982215 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Add extra uvloop install + import support if in python env (#2258) - Fix --experimental-string-processing crash when matching parens are not found (#2283) - Make sure to split lines that start with a string operator (#2286) +- Fix regular expression that black uses to identify f-expressions (#2287) ### _Blackd_ diff --git a/src/black/trans.py b/src/black/trans.py index 80e88a2d2fb..fd0de727620 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -971,8 +971,8 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): | \{\{ | \}\} | (?R) - )+? - (? TMatchResult: diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index bd7b6351321..cd8053f9eb5 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -407,6 +407,12 @@ def _legacy_listen_examples(): ) +temp_msg = ( + f"{f'{humanize_number(pos)}.': <{pound_len+2}} " + f"{balance: <{bal_len + 5}} " + f"<<{author.display_name}>>\n" +) + assert str(suffix_arr) == ( "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " "'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', " @@ -942,6 +948,12 @@ def _legacy_listen_examples(): ) +temp_msg = ( + f"{f'{humanize_number(pos)}.': <{pound_len+2}} " + f"{balance: <{bal_len + 5}} " + f"<<{author.display_name}>>\n" +) + assert ( str(suffix_arr) == "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', " From cf75673e1a2c993025a2113ce194d5c65f311c85 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 31 May 2021 07:25:54 -0700 Subject: [PATCH 270/680] Update CHANGES.md for 21.5b2 release (#2290) * Update CHANGES.md for 21.5b2 release --- CHANGES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39256982215..4bf08275cf8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.5b2 ### _Black_ @@ -21,7 +21,8 @@ ### _Packaging_ -- Release self-contained macOS binaries as part of the GitHub release pipeline (#2198) +- Release self-contained x86_64 MacOS binaries as part of the GitHub release pipeline + (#2198) - Always build binaries with the latest available Python (#2260) ### Documentation From a4e35b314977baae2e930abd24fa1013c7235e39 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Mon, 31 May 2021 20:57:23 -0400 Subject: [PATCH 271/680] Correct max string length calculation when there are string operators (#2292) PR #2286 did not fix the edge-cases (e.g. when the string is just long enough to cause a line to be 89 characters long). This PR corrects that mistake. --- CHANGES.md | 6 ++++++ src/black/trans.py | 28 ++++++++++++++------------- tests/data/long_strings__edge_case.py | 19 ++++++++++++++++++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4bf08275cf8..de67943d29f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Correct max string length calculation when there are string operators (#2292) + ## 21.5b2 ### _Black_ diff --git a/src/black/trans.py b/src/black/trans.py index fd0de727620..bc6e93b01b4 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -738,6 +738,18 @@ class BaseStringSplitter(StringTransformer): * The target string is not a multiline (i.e. triple-quote) string. """ + STRING_OPERATORS = [ + token.EQEQUAL, + token.GREATER, + token.GREATEREQUAL, + token.LESS, + token.LESSEQUAL, + token.NOTEQUAL, + token.PERCENT, + token.PLUS, + token.STAR, + ] + @abstractmethod def do_splitter_match(self, line: Line) -> TMatchResult: """ @@ -847,9 +859,9 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: p_idx -= 1 P = LL[p_idx] - if P.type == token.PLUS: - # WMA4 a space and a '+' character (e.g. `+ STRING`). - offset += 2 + if P.type in self.STRING_OPERATORS: + # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`). + offset += len(str(P)) + 1 if P.type == token.COMMA: # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`]. @@ -952,16 +964,6 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter): CustomSplit objects and add them to the custom split map. """ - STRING_OPERATORS = [ - token.PLUS, - token.STAR, - token.EQEQUAL, - token.NOTEQUAL, - token.LESS, - token.LESSEQUAL, - token.GREATER, - token.GREATEREQUAL, - ] MIN_SUBSTR_SIZE = 6 # Matches an "f-expression" (e.g. {var}) that might be found in an f-string. RE_FEXPR = r""" diff --git a/tests/data/long_strings__edge_case.py b/tests/data/long_strings__edge_case.py index 6919db5a80b..07c27537191 100644 --- a/tests/data/long_strings__edge_case.py +++ b/tests/data/long_strings__edge_case.py @@ -29,6 +29,9 @@ ) return f'{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaa' return f'{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa' +assert str(result) == "This long string should be split at some point right close to or around hereeeeeee" +assert str(result) < "This long string should be split at some point right close to or around hereeeeee" +assert "A format string: %s" % "This long string should be split at some point right close to or around hereeeeeee" != result # output @@ -108,3 +111,19 @@ f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaa" ) return f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" +assert ( + str(result) + == "This long string should be split at some point right close to or around" + " hereeeeeee" +) +assert ( + str(result) + < "This long string should be split at some point right close to or around" + " hereeeeee" +) +assert ( + "A format string: %s" + % "This long string should be split at some point right close to or around" + " hereeeeeee" + != result +) From 4005246f86d1459e1123ac399721054182e9fef6 Mon Sep 17 00:00:00 2001 From: Stefan Foulis Date: Tue, 1 Jun 2021 03:45:50 +0200 Subject: [PATCH 272/680] Add `version` to github action (and rewrite the whole thing while at it) (#1940) Commit history before merge: * Add black_version to github action * Merge upstream/main into this branch * Add version support for the Black action pt.2 Since we're moving to a composite based action, quite a few changes were made. 1) Support was added for all OSes (Windows was painful). 2) Isolation from the rest of the workflow had to be done manually with a virtual environment. Other noteworthy changes: - Rewrote basically all of the logic and put it in a Python script for easy testing (not doing it here tho cause I'm lazy and I can't think of a reasonable way of testing it). - Renamed `black_version` to `version` to better fit the existing input naming scheme. - Added support for log groups, this makes our action's output a bit more fancy (I may or may have not added some debug output too). * Add more to and sorta rewrite the Action's docs Reflect compatability and gotchas. * Add CHANGELOG entry * Merge main into this branch * Remove debug; address typos; clean up action.yml Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 7 ++++- action.yml | 43 ++++++++++++++++++++++++++--- action/Dockerfile | 10 ------- action/entrypoint.sh | 9 ------ action/main.py | 39 ++++++++++++++++++++++++++ docs/integrations/github_actions.md | 23 ++++++++++++--- 6 files changed, 103 insertions(+), 28 deletions(-) delete mode 100644 action/Dockerfile delete mode 100755 action/entrypoint.sh create mode 100644 action/main.py diff --git a/CHANGES.md b/CHANGES.md index de67943d29f..81a6d9f6668 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,7 +25,12 @@ - Add a lower bound for the `aiohttp-cors` dependency. Only 0.4.0 or higher is supported. (#2231) -### _Packaging_ +### Integrations + +- The official Black action now supports choosing what version to use, and supports the + major 3 OSes. (#1940) + +### Packaging - Release self-contained x86_64 MacOS binaries as part of the GitHub release pipeline (#2198) diff --git a/action.yml b/action.yml index 827e971801b..ddf07933a3e 100644 --- a/action.yml +++ b/action.yml @@ -4,21 +4,56 @@ author: "Łukasz Langa and contributors to Black" inputs: options: description: - "Options passed to black. Use `black --help` to see available options. Default: + "Options passed to Black. Use `black --help` to see available options. Default: '--check'" required: false default: "--check --diff" src: - description: "Source to run black. Default: '.'" + description: "Source to run Black. Default: '.'" required: false default: "." black_args: description: "[DEPRECATED] Black input arguments." required: false default: "" + deprecationMessage: + "Input `with.black_args` is deprecated. Use `with.options` and `with.src` instead." + version: + description: 'Python Version specifier (PEP440) - e.g. "21.5b1"' + required: false + default: "" branding: color: "black" icon: "check-circle" runs: - using: "docker" - image: "action/Dockerfile" + 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 + else + echo $entrypoint | python3 + fi + env: + # TODO: Remove once https://github.com/actions/runner/issues/665 is fixed. + INPUT_OPTIONS: ${{ inputs.options }} + INPUT_SRC: ${{ inputs.src }} + INPUT_BLACK_ARGS: ${{ inputs.black_args }} + INPUT_VERSION: ${{ inputs.version }} + pythonioencoding: utf-8 + shell: bash diff --git a/action/Dockerfile b/action/Dockerfile deleted file mode 100644 index eb2209940db..00000000000 --- a/action/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM python:3 - -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 - -RUN pip install --upgrade --no-cache-dir black - -COPY entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/action/entrypoint.sh b/action/entrypoint.sh deleted file mode 100755 index 30bf4eb688f..00000000000 --- a/action/entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -e - -if [ -n "$INPUT_BLACK_ARGS" ]; then - echo '::warning::Input `with.black_args` is deprecated. Use `with.options` and `with.src` instead.' - black $INPUT_BLACK_ARGS - exit $? -fi - -black $INPUT_OPTIONS $INPUT_SRC diff --git a/action/main.py b/action/main.py new file mode 100644 index 00000000000..fde312553bf --- /dev/null +++ b/action/main.py @@ -0,0 +1,39 @@ +import os +import shlex +import sys +from pathlib import Path +from subprocess import run, PIPE, STDOUT + +ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"]) +ENV_PATH = ACTION_PATH / ".black-env" +ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin") +OPTIONS = os.getenv("INPUT_OPTIONS", default="") +SRC = os.getenv("INPUT_SRC", default="") +BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="") +VERSION = os.getenv("INPUT_VERSION", default="") + +run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True) + +req = "black[colorama,python2]" +if VERSION: + req += f"=={VERSION}" +pip_proc = run( + [str(ENV_BIN / "python"), "-m", "pip", "install", req], + stdout=PIPE, + stderr=STDOUT, + encoding="utf-8", +) +if pip_proc.returncode: + print(pip_proc.stdout) + print("::error::Failed to install Black.", flush=True) + sys.exit(pip_proc.returncode) + + +base_cmd = [str(ENV_BIN / "black")] +if BLACK_ARGS: + # TODO: remove after a while since this is deprecated in favour of SRC + OPTIONS. + proc = run([*base_cmd, *shlex.split(BLACK_ARGS)]) +else: + proc = run([*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)]) + +sys.exit(proc.returncode) diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index 9e8cf436453..d293c40dadf 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -3,6 +3,14 @@ You can use _Black_ within a GitHub Actions workflow without setting your own Python environment. Great for enforcing that your code matches the _Black_ code style. +## Compatiblity + +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. + ## Usage Create a file named `.github/workflows/black.yml` inside your repository with: @@ -17,19 +25,26 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - uses: psf/black@stable ``` We recommend the use of the `@stable` tag, but per version tags also exist if you prefer -that. +that. Note that the action's version you select is independent of the version of _Black_ +the action will use. + +The version of _Black_ the action will use can be configured via `version`. The action +defaults to the latest release available on PyPI. Only versions available from PyPI are +supported, so no commit SHAs or branch names. + +You can also configure the arguments passed to _Black_ via `options` (defaults to +`'--check --diff'`) and `src` (default is `'.'`) -You may use `options` (Default is `'--check --diff'`) and `src` (Default is `'.'`) as -follows: +Here's an example configuration: ```yaml - uses: psf/black@stable with: options: "--check --verbose" src: "./src" + version: "21.5b1" ``` From fdc4b67433906c2b8beeb349366a69befc37aafe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:08:17 -0700 Subject: [PATCH 273/680] Bump urllib3 from 1.26.4 to 1.26.5 (#2298) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 230 +++++++++++++++++++++------------------------------ 1 file changed, 92 insertions(+), 138 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b6dfe438e9e..4f0b51e7d18 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -78,7 +78,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -86,7 +85,6 @@ "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": { @@ -101,7 +99,6 @@ "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" }, "click": { @@ -118,17 +115,14 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "python_version <": "3.7", - "version": "==0.6", - "version >": "0.6" + "version": "==0.6" }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "markers": "python_version >= '3.4'", - "version": "==3.1" + "version": "==3.2" }, "multidict": { "hashes": [ @@ -170,7 +164,6 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy-extensions": { @@ -236,14 +229,6 @@ "index": "pypi", "version": "==2021.4.4" }, - "setuptools-scm": { - "hashes": [ - "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", - "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -295,9 +280,7 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.8", - "version": "==3.10.0.0", - "version >=": "3.7.4" + "version": "==3.10.0.0" }, "yarl": { "hashes": [ @@ -339,7 +322,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -415,7 +397,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -423,7 +404,6 @@ "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": { @@ -431,7 +411,6 @@ "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" }, "black": { @@ -446,27 +425,35 @@ "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.3.0" }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2020.12.5" + "version": "==2021.5.30" }, "cffi": { "hashes": [ "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373", + "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69", + "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f", "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05", "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0", "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7", + "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f", "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76", "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed", "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", @@ -474,6 +461,7 @@ "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55", "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", @@ -491,8 +479,10 @@ "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc", "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333", "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" ], @@ -500,18 +490,16 @@ }, "cfgv": { "hashes": [ - "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", - "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" + "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1", + "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.2.0" + "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" }, "click": { @@ -527,7 +515,6 @@ "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": { @@ -603,20 +590,18 @@ "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" ], - "markers": "python_version >= '3.6'", "version": "==3.4.7" }, "distlib": { "hashes": [ - "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", - "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" + "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736", + "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c" ], - "version": "==0.3.1" + "version": "==0.3.2" }, "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", - "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -647,35 +632,31 @@ }, "identify": { "hashes": [ - "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e", - "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8" + "sha256:92d6ad08eca19ceb17576733759944b94c0761277ddc3acf65e75e57ef190e32", + "sha256:c29e74c3671fe9537715cb695148231d777170ca1498e1c30c675d4ea782afe9" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.4" + "version": "==2.2.7" }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "markers": "python_version >= '3.4'", - "version": "==3.1" + "version": "==3.2" }, "imagesize": { "hashes": [ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "importlib-metadata": { "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786", + "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" + "version": "==4.4.0" }, "jeepney": { "hashes": [ @@ -687,18 +668,16 @@ }, "jinja2": { "hashes": [ - "sha256:2f2de5285cf37f33d33ecd4a9080b75c87cd0c1994d5a9c6df17131ea1f049c6", - "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5" + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.0" + "version": "==3.0.1" }, "keyring": { "hashes": [ "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" ], - "markers": "python_version >= '3.6'", "version": "==23.0.1" }, "markdown-it-py": { @@ -706,48 +685,46 @@ "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3", "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389" ], - "markers": "python_version ~= '3.6'", "version": "==1.1.0" }, "markupsafe": { "hashes": [ - "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95", - "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f", - "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d", - "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc", - "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0", - "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901", - "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66", - "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63", - "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b", - "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5", - "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c", - "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1", - "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05", - "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf", - "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527", - "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb", - "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb", - "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2", - "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730", - "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1", - "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75", - "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b", - "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b", - "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715", - "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b", - "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8", - "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96", - "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348", - "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958", - "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd", - "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6", - "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20", - "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf", - "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -761,7 +738,6 @@ "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c", "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f" ], - "markers": "python_version ~= '3.6'", "version": "==0.2.8" }, "multidict": { @@ -804,7 +780,6 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy": { @@ -863,7 +838,6 @@ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pathspec": { @@ -894,7 +868,6 @@ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, "pycparser": { @@ -902,7 +875,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -910,7 +882,6 @@ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, "pygments": { @@ -918,7 +889,6 @@ "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], - "markers": "python_version >= '3.5'", "version": "==2.9.0" }, "pyparsing": { @@ -926,7 +896,6 @@ "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" }, "pytz": { @@ -968,7 +937,6 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], - "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" }, "readme-renderer": { @@ -1031,7 +999,6 @@ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.25.1" }, "requests-toolbelt": { @@ -1061,7 +1028,7 @@ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==6.0.1" }, "six": { @@ -1069,7 +1036,6 @@ "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": { @@ -1100,7 +1066,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -1108,23 +1073,20 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", - "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", + "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" + "version": "==2.0.0" }, "sphinxcontrib-jsmath": { "hashes": [ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-programoutput": { @@ -1140,16 +1102,14 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", - "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", + "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" ], - "markers": "python_version >= '3.5'", - "version": "==1.1.4" + "version": "==1.1.5" }, "toml": { "hashes": [ @@ -1161,11 +1121,10 @@ }, "tqdm": { "hashes": [ - "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", - "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" + "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", + "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.60.0" + "version": "==4.61.0" }, "twine": { "hashes": [ @@ -1218,25 +1177,22 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.8", - "version": "==3.10.0.0", - "version >=": "3.7.4" + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", + "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" ], - "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.4" + "index": "pypi", + "version": "==1.26.5" }, "virtualenv": { "hashes": [ - "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543", - "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4" + "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", + "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4.6" + "version": "==20.4.7" }, "webencodings": { "hashes": [ @@ -1293,7 +1249,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" }, "zipp": { @@ -1301,7 +1256,6 @@ "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], - "markers": "python_version >= '3.6'", "version": "==3.4.1" } } From 7567cdf3b4f32d4fb12bd5ca0da838f7ff252cfc Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 2 Jun 2021 04:55:21 +0300 Subject: [PATCH 274/680] Code Flag Options (#2259) Properly handles the diff, color, and fast option when black is run with the `--code` option. Closes #2104, closes #1801. --- AUTHORS.md | 1 + CHANGES.md | 2 + docs/usage_and_configuration/the_basics.md | 7 - src/black/__init__.py | 119 ++++++++++++----- src/black/files.py | 2 +- tests/test_black.py | 144 +++++++++++++++++++++ 6 files changed, 233 insertions(+), 42 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index cb79dec3de3..8d112ea6795 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -68,6 +68,7 @@ Multiple contributions by: - Gustavo Camargo - hauntsaninja - [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) +- [Hassan Abouelela](mailto:hassan@hassanamr.com) - [Heaford](mailto:dan@heaford.com) - [Hugo Barrera](mailto::hugo@barrera.io) - Hugo van Kemenade diff --git a/CHANGES.md b/CHANGES.md index 81a6d9f6668..e7851637836 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) +- Fixed option usage when using the `--code` flag (#2259) - Add extra uvloop install + import support if in python env (#2258) - Fix --experimental-string-processing crash when matching parens are not found (#2283) - Make sure to split lines that start with a string operator (#2286) @@ -43,6 +44,7 @@ - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) - Add new Frequently Asked Questions page (#2247) +- Removed safety checks warning for the `--code` option (#2259) - Fix encoding + symlink issues preventing proper build on Windows (#2262) ## 21.5b1 diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 0b2cd3b3544..c5e17e5a521 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -68,13 +68,6 @@ $ black --code "print ( 'hello, world' )" print("hello, world") ``` -```{warning} ---check, --diff, and --safe / --fast have no effect when using -c / --code. Safety -checks normally turned on by default that verify _Black_'s output are disabled as well. -This is a bug which we intend to fix eventually. More details can be found in this [bug -report](https://github.com/psf/black/issues/2104). -``` - ### Writeback and reporting By default _Black_ reformats the files given and/or found in place. Sometimes you need diff --git a/src/black/__init__.py b/src/black/__init__.py index d83f0e54a72..1d0ad7d5ddd 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -384,47 +384,61 @@ def main( ) if config and verbose: out(f"Using configuration from {config}.", bold=False, fg="blue") + if code is not None: - print(format_str(code, mode=mode)) - ctx.exit(0) - report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) - sources = get_sources( - ctx=ctx, - src=src, - quiet=quiet, - verbose=verbose, - include=include, - exclude=exclude, - extend_exclude=extend_exclude, - force_exclude=force_exclude, - report=report, - stdin_filename=stdin_filename, - ) + # Run in quiet mode by default with -c; the extra output isn't useful. + # You can still pass -v to get verbose output. + quiet = True - path_empty( - sources, - "No Python files are present to be formatted. Nothing to do 😴", - quiet, - verbose, - ctx, - ) + report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) - if len(sources) == 1: - reformat_one( - src=sources.pop(), - fast=fast, - write_back=write_back, - mode=mode, - report=report, + if code is not None: + reformat_code( + content=code, fast=fast, write_back=write_back, mode=mode, report=report ) else: - reformat_many( - sources=sources, fast=fast, write_back=write_back, mode=mode, report=report + sources = get_sources( + ctx=ctx, + src=src, + quiet=quiet, + verbose=verbose, + include=include, + exclude=exclude, + extend_exclude=extend_exclude, + force_exclude=force_exclude, + report=report, + stdin_filename=stdin_filename, ) + path_empty( + sources, + "No Python files are present to be formatted. Nothing to do 😴", + quiet, + verbose, + ctx, + ) + + if len(sources) == 1: + reformat_one( + src=sources.pop(), + fast=fast, + write_back=write_back, + mode=mode, + report=report, + ) + else: + reformat_many( + sources=sources, + fast=fast, + write_back=write_back, + mode=mode, + report=report, + ) + if verbose or not quiet: out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨") - click.secho(str(report), err=True) + if code is None: + click.secho(str(report), err=True) ctx.exit(report.return_code) @@ -512,6 +526,30 @@ def path_empty( ctx.exit(0) +def reformat_code( + content: str, fast: bool, write_back: WriteBack, mode: Mode, report: Report +) -> None: + """ + Reformat and print out `content` without spawning child processes. + Similar to `reformat_one`, but for string content. + + `fast`, `write_back`, and `mode` options are passed to + :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. + """ + path = Path("") + try: + changed = Changed.NO + if format_stdin_to_stdout( + content=content, fast=fast, write_back=write_back, mode=mode + ): + changed = Changed.YES + report.done(path, changed) + except Exception as exc: + if report.verbose: + traceback.print_exc() + report.failed(path, str(exc)) + + def reformat_one( src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report" ) -> None: @@ -720,16 +758,27 @@ def format_file_in_place( def format_stdin_to_stdout( - fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode + fast: bool, + *, + content: Optional[str] = None, + write_back: WriteBack = WriteBack.NO, + mode: Mode, ) -> bool: """Format file on stdin. Return True if changed. + If content is None, it's read from sys.stdin. + If `write_back` is YES, write reformatted code back to stdout. If it is DIFF, write a diff to stdout. The `mode` argument is passed to :func:`format_file_contents`. """ then = datetime.utcnow() - src, encoding, newline = decode_bytes(sys.stdin.buffer.read()) + + if content is None: + src, encoding, newline = decode_bytes(sys.stdin.buffer.read()) + else: + src, encoding, newline = content, "utf-8", "" + dst = src try: dst = format_file_contents(src, fast=fast, mode=mode) @@ -743,6 +792,8 @@ def format_stdin_to_stdout( sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True ) if write_back == WriteBack.YES: + # Make sure there's a newline after the content + dst += "" if dst[-1] == "\n" else "\n" f.write(dst) elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): now = datetime.utcnow() diff --git a/src/black/files.py b/src/black/files.py index de516156605..b9cefd317e0 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -38,7 +38,7 @@ def find_project_root(srcs: Sequence[str]) -> Path: project root, the root of the file system is returned. """ if not srcs: - return Path("/").resolve() + srcs = [str(Path.cwd().resolve())] path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] diff --git a/tests/test_black.py b/tests/test_black.py index 098a9ec9157..f0a14aa2da4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -34,6 +34,7 @@ from black import Feature, TargetVersion from black.cache import get_cache_file from black.debug import DebugVisitor +from black.output import diff, color_diff from black.report import Report import black.files @@ -63,6 +64,9 @@ T = TypeVar("T") R = TypeVar("R") +# Match the time output in a diff, but nothing else +DIFF_TIME = re.compile(r"\t[\d-:+\. ]+") + @contextmanager def cache_dir(exists: bool = True) -> Iterator[Path]: @@ -2069,6 +2073,146 @@ def test_docstring_reformat_for_py27(self) -> None: actual = result.output self.assertFormatEqual(actual, expected) + @staticmethod + def compare_results( + result: click.testing.Result, expected_value: str, expected_exit_code: int + ) -> None: + """Helper method to test the value and exit code of a click Result.""" + assert ( + result.output == expected_value + ), "The output did not match the expected value." + assert result.exit_code == expected_exit_code, "The exit code is incorrect." + + def test_code_option(self) -> None: + """Test the code option with no changes.""" + code = 'print("Hello world")\n' + args = ["--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, code, 0) + + def test_code_option_changed(self) -> None: + """Test the code option when changes are required.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + + args = ["--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, formatted, 0) + + def test_code_option_check(self) -> None: + """Test the code option when check is passed.""" + args = ["--check", "--code", 'print("Hello world")\n'] + result = CliRunner().invoke(black.main, args) + self.compare_results(result, "", 0) + + def test_code_option_check_changed(self) -> None: + """Test the code option when changes are required, and check is passed.""" + args = ["--check", "--code", "print('hello world')"] + result = CliRunner().invoke(black.main, args) + self.compare_results(result, "", 1) + + def test_code_option_diff(self) -> None: + """Test the code option when diff is passed.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + result_diff = diff(code, formatted, "STDIN", "STDOUT") + + args = ["--diff", "--code", code] + result = CliRunner().invoke(black.main, args) + + # Remove time from diff + output = DIFF_TIME.sub("", result.output) + + assert output == result_diff, "The output did not match the expected value." + assert result.exit_code == 0, "The exit code is incorrect." + + def test_code_option_color_diff(self) -> None: + """Test the code option when color and diff are passed.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + + result_diff = diff(code, formatted, "STDIN", "STDOUT") + result_diff = color_diff(result_diff) + + args = ["--diff", "--color", "--code", code] + result = CliRunner().invoke(black.main, args) + + # Remove time from diff + output = DIFF_TIME.sub("", result.output) + + assert output == result_diff, "The output did not match the expected value." + assert result.exit_code == 0, "The exit code is incorrect." + + 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 + with patch.object(black, "assert_equivalent", side_effect=AssertionError): + code = 'print("Hello world")' + error_msg = f"{code}\nerror: cannot format : \n" + + args = ["--safe", "--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, error_msg, 123) + + def test_code_option_fast(self) -> None: + """Test that the code option ignores errors when the sanity checks fail.""" + # Patch black.assert_equivalent to ensure the sanity checks fail + with patch.object(black, "assert_equivalent", side_effect=AssertionError): + code = 'print("Hello world")' + formatted = black.format_str(code, mode=DEFAULT_MODE) + + args = ["--fast", "--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, formatted, 0) + + def test_code_option_config(self) -> None: + """ + Test that the code option finds the pyproject.toml in the current directory. + """ + with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: + # Make sure we are in the project root with the pyproject file + if not Path("tests").exists(): + os.chdir("..") + + args = ["--code", "print"] + CliRunner().invoke(black.main, args) + + pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve() + assert ( + len(parse.mock_calls) >= 1 + ), "Expected config parse to be called with the current directory." + + _, call_args, _ = parse.mock_calls[0] + assert ( + call_args[0].lower() == str(pyproject_path).lower() + ), "Incorrect config loaded." + + 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: + # Make sure we are in the tests directory + if Path("tests").exists(): + os.chdir("tests") + + args = ["--code", "print"] + CliRunner().invoke(black.main, args) + + pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve() + assert ( + len(parse.mock_calls) >= 1 + ), "Expected config parse to be called with the current directory." + + _, call_args, _ = parse.mock_calls[0] + assert ( + call_args[0].lower() == str(pyproject_path).lower() + ), "Incorrect config loaded." + with open(black.__file__, "r", encoding="utf-8") as _bf: black_source_lines = _bf.readlines() From 5de8c5f2f798d97133de3a1b391c04813c314c48 Mon Sep 17 00:00:00 2001 From: Cooper Ry Lees Date: Tue, 1 Jun 2021 19:45:03 -0700 Subject: [PATCH 275/680] Move `--code` #2259 change log to correct unlreased section of CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e7851637836..e9775e0ac42 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Black_ - Correct max string length calculation when there are string operators (#2292) +- Fixed option usage when using the `--code` flag (#2259) ## 21.5b2 @@ -15,7 +16,6 @@ - Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply `.gitignore` rules like `git` does) (#2225) - Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227) -- Fixed option usage when using the `--code` flag (#2259) - Add extra uvloop install + import support if in python env (#2258) - Fix --experimental-string-processing crash when matching parens are not found (#2283) - Make sure to split lines that start with a string operator (#2286) From f2a3fee15c3bc0f4068b2b2fb64f255b417cb4ef Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 1 Jun 2021 20:01:02 -0700 Subject: [PATCH 276/680] remove unnecessary docs changelog --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e9775e0ac42..3427fe8e391 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,7 +44,6 @@ - Fix typos discovered by codespell (#2228) - Fix Vim plugin installation instructions. (#2235) - Add new Frequently Asked Questions page (#2247) -- Removed safety checks warning for the `--code` option (#2259) - Fix encoding + symlink issues preventing proper build on Windows (#2262) ## 21.5b1 From df1c86cbe7cf0727d81e526e904b0752d7371da0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 3 Jun 2021 10:13:55 -0700 Subject: [PATCH 277/680] don't uvloop.install on import (#2303) --- CHANGES.md | 1 + src/black/__init__.py | 11 ++--------- src/black/concurrency.py | 15 +++++++++++++++ src/blackd/__init__.py | 10 ++-------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3427fe8e391..e4fa25c90f4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Correct max string length calculation when there are string operators (#2292) - Fixed option usage when using the `--code` flag (#2259) +- Do not call `uvloop.install()` when _Black_ is used as a library (#2303) ## 21.5b2 diff --git a/src/black/__init__.py b/src/black/__init__.py index 1d0ad7d5ddd..d95e9b13bb9 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -38,7 +38,7 @@ from black.mode import 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 +from black.concurrency import cancel, shutdown, maybe_install_uvloop from black.output import dump_to_file, diff, color_diff, out, err from black.report import Report, Changed from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml @@ -54,14 +54,6 @@ from _black_version import version as __version__ -# If our environment has uvloop installed lets use it -try: - import uvloop - - uvloop.install() -except ImportError: - pass - # types FileContent = str Encoding = str @@ -1112,6 +1104,7 @@ def patch_click() -> None: def patched_main() -> None: + maybe_install_uvloop() freeze_support() patch_click() main() diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 119a9a71faf..69d79f534e8 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -6,6 +6,21 @@ from black.output import err +def maybe_install_uvloop() -> None: + """If our environment has uvloop installed we use it. + + This is called only from command-line entry points to avoid + interfering with the parent process if Black is used as a library. + + """ + try: + import uvloop + + uvloop.install() + except ImportError: + pass + + def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: """asyncio signal handler that cancels all `tasks` and reports to stderr.""" err("Aborted!") diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 10b616894da..3e2a7e7c30f 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -20,16 +20,9 @@ sys.exit(-1) import black +from black.concurrency import maybe_install_uvloop import click -# If our environment has uvloop installed lets use it -try: - import uvloop - - uvloop.install() -except ImportError: - pass - from _black_version import version as __version__ # This is used internally by tests to shut down the server prematurely @@ -210,6 +203,7 @@ 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() From a2b5ba2a3a97eb3a0b5130a5b317bb750c1624cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Thu, 3 Jun 2021 23:09:41 +0300 Subject: [PATCH 278/680] Add option to require a specific version to be running (#2300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1246: This PR adds a new option (and automatically a toml entry, hooray for existing configuration management 🎉) to require a specific version of Black to be running. For example: `black --required-version 20.8b -c "format = 'this'"` Execution fails straight away if it doesn't match `__version__`. --- CHANGES.md | 1 + docs/usage_and_configuration/the_basics.md | 15 ++++++++++++- src/black/__init__.py | 26 ++++++++++++++++++---- tests/test_black.py | 10 +++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e4fa25c90f4..16d9ebc3fe5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Correct max string length calculation when there are string operators (#2292) - Fixed option usage when using the `--code` flag (#2259) - Do not call `uvloop.install()` when _Black_ is used as a library (#2303) +- Added `--required-version` option to require a specific version to be running (#2300) ## 21.5b2 diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index c5e17e5a521..474ad669cd1 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -167,7 +167,7 @@ $ black src/ -q error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio ``` -### Getting the version +### Versions You can check the version of _Black_ you have installed using the `--version` flag. @@ -176,6 +176,19 @@ $ black --version black, version 21.5b0 ``` +An option to require a specific version to be running is also provided. + +```console +$ black --required-version 21.5b2 -c "format = 'this'" +format = "this" +$ black --required-version 31.5b2 -c "still = 'beta?!'" +Oh no! 💥 💔 💥 The required version does not match the running version! +``` + +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. + ## Configuration via a file _Black_ is able to read project-specific default values for its command line options diff --git a/src/black/__init__.py b/src/black/__init__.py index d95e9b13bb9..a985926afa5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -241,6 +241,14 @@ def validate_regex( is_flag=True, help="If --fast given, skip temporary sanity checks. [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)." + ), +) @click.option( "--include", type=str, @@ -351,6 +359,7 @@ def main( experimental_string_processing: bool, quiet: bool, verbose: bool, + required_version: str, include: Pattern, exclude: Optional[Pattern], extend_exclude: Optional[Pattern], @@ -360,6 +369,17 @@ def main( config: Optional[str], ) -> None: """The uncompromising code formatter.""" + if config and verbose: + out(f"Using configuration from {config}.", bold=False, fg="blue") + + error_msg = "Oh no! 💥 💔 💥" + if required_version and required_version != __version__: + err( + f"{error_msg} The required version `{required_version}` does not match" + f" the running version `{__version__}`!" + ) + ctx.exit(1) + write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) if target_version: versions = set(target_version) @@ -374,8 +394,6 @@ def main( magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, ) - if config and verbose: - out(f"Using configuration from {config}.", bold=False, fg="blue") if code is not None: # Run in quiet mode by default with -c; the extra output isn't useful. @@ -428,9 +446,9 @@ def main( ) if verbose or not quiet: - out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨") + out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") if code is None: - click.secho(str(report), err=True) + click.echo(str(report), err=True) ctx.exit(report.return_code) diff --git a/tests/test_black.py b/tests/test_black.py index f0a14aa2da4..455cb33e827 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1796,6 +1796,16 @@ 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: + self.invokeBlack( + ["--required-version", "20.99b"], exit_code=1, ignore_config=True + ) + def test_preserves_line_endings(self) -> None: with TemporaryDirectory() as workspace: test_file = Path(workspace) / "test.py" From c53b3ad8fa62eb124d017a23bdff5a2bfa9890af Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 3 Jun 2021 21:26:21 -0400 Subject: [PATCH 279/680] Go back to single core for test suite on CI (#2305) The random asyncio bug is just too frequent and annoying to be worth the speed improvements. Our test suite is already quite fast. Random test failures hurt for 3 reasons, 1) they are discouraging for new contributors who won't understand it's out of their control, 2) it's annoying and time consuming to rerun the workflow, and 3) it makes single job failures feel less important (even they should be treated as important!). --- .github/workflows/test.yml | 2 +- tox.ini | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef8debb3fb7..1db9f5d6d93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: - name: Unit tests run: | - tox -e py -- -v --color=yes + tox -e ci-py -- -v --color=yes - name: Publish coverage to Coveralls # If pushed / is a pull request against main repo AND diff --git a/tox.ini b/tox.ini index 2379500f55a..3ea4da8eac2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,23 @@ [tox] -envlist = py{36,37,38,39},fuzz +envlist = {,ci-}py{36,37,38,39},fuzz [testenv] setenv = PYTHONPATH = {toxinidir}/src skip_install = True deps = -r{toxinidir}/test_requirements.txt +; parallelization is disabled on CI because pytest-dev/pytest-xdist#620 occurs too frequently +; local runs can stay parallelized since they aren't rolling the dice so many times as like on CI commands = pip install -e .[d] coverage erase - pytest tests --run-optional no_python2 --numprocesses auto --cov {posargs} + pytest tests --run-optional no_python2 \ + !ci: --numprocesses auto \ + --cov {posargs} pip install -e .[d,python2] - pytest tests --run-optional python2 --numprocesses auto --cov --cov-append {posargs} + pytest tests --run-optional python2 \ + !ci: --numprocesses auto \ + --cov --cov-append {posargs} coverage report [testenv:fuzz] From 6380b9f2f6e53748360a120ed9acb874a35885e0 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Mon, 7 Jun 2021 10:01:57 -0400 Subject: [PATCH 280/680] Account for += assignment when deciding whether to split string (#2312) Fixes #2294 --- CHANGES.md | 1 + src/black/trans.py | 2 +- tests/data/long_strings__edge_case.py | 11 +++++++++++ tests/data/long_strings__regression.py | 20 ++++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 16d9ebc3fe5..84f156d903d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Account for += assignment when deciding whether to split string (#2312) - Correct max string length calculation when there are string operators (#2292) - Fixed option usage when using the `--code` flag (#2259) - Do not call `uvloop.install()` when _Black_ is used as a library (#2303) diff --git a/src/black/trans.py b/src/black/trans.py index bc6e93b01b4..d56205d4598 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -867,7 +867,7 @@ def _get_max_string_length(self, line: Line, string_idx: int) -> int: # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`]. offset += 3 - if P.type in [token.COLON, token.EQUAL, token.NAME]: + if P.type in [token.COLON, token.EQUAL, token.PLUSEQUAL, token.NAME]: # This conditional branch is meant to handle dictionary keys, # variable assignments, 'return STRING' statement lines, and # 'else STRING' ternary expression lines. diff --git a/tests/data/long_strings__edge_case.py b/tests/data/long_strings__edge_case.py index 07c27537191..2bc0b6ed328 100644 --- a/tests/data/long_strings__edge_case.py +++ b/tests/data/long_strings__edge_case.py @@ -32,6 +32,9 @@ assert str(result) == "This long string should be split at some point right close to or around hereeeeeee" assert str(result) < "This long string should be split at some point right close to or around hereeeeee" assert "A format string: %s" % "This long string should be split at some point right close to or around hereeeeeee" != result +msg += "This long string should be wrapped in parens at some point right around hereeeee" +msg += "This long string should be split at some point right close to or around hereeeeeeee" +msg += "This long string should not be split at any point ever since it is just righttt" # output @@ -127,3 +130,11 @@ " hereeeeeee" != result ) +msg += ( + "This long string should be wrapped in parens at some point right around hereeeee" +) +msg += ( + "This long string should be split at some point right close to or around" + " hereeeeeeee" +) +msg += "This long string should not be split at any point ever since it is just righttt" diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index cd8053f9eb5..d46f96a82d0 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -407,6 +407,16 @@ def _legacy_listen_examples(): ) +class X: + async def foo(self): + msg = "" + for candidate in CANDIDATES: + msg += ( + "**{candidate.object_type} {candidate.rev}**" + " - {candidate.description}\n" + ) + + temp_msg = ( f"{f'{humanize_number(pos)}.': <{pound_len+2}} " f"{balance: <{bal_len + 5}} " @@ -948,6 +958,16 @@ def _legacy_listen_examples(): ) +class X: + async def foo(self): + msg = "" + for candidate in CANDIDATES: + msg += ( + "**{candidate.object_type} {candidate.rev}**" + " - {candidate.description}\n" + ) + + temp_msg = ( f"{f'{humanize_number(pos)}.': <{pound_len+2}} " f"{balance: <{bal_len + 5}} " From 99b68e59ce0424a86215cc9ca0cb46e481d2a6fe Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Mon, 7 Jun 2021 10:03:39 -0400 Subject: [PATCH 281/680] Fix incorrect custom breakpoint indices when string group contains fake f-strings (#2311) Fixes #2293 --- CHANGES.md | 2 + src/black/trans.py | 33 +++++++---- tests/data/long_strings__regression.py | 78 ++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 84f156d903d..02b3fdf75d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,8 @@ - Fixed option usage when using the `--code` flag (#2259) - Do not call `uvloop.install()` when _Black_ is used as a library (#2303) - Added `--required-version` option to require a specific version to be running (#2300) +- Fix incorrect custom breakpoint indices when string group contains fake f-strings + (#2311) ## 21.5b2 diff --git a/src/black/trans.py b/src/black/trans.py index d56205d4598..4fb2c4d0144 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1151,21 +1151,32 @@ def more_splits_should_be_made() -> bool: # --- Construct `next_value` next_value = rest_value[:break_idx] + QUOTE + + # HACK: The following 'if' statement is a hack to fix the custom + # breakpoint index in the case of either: (a) substrings that were + # f-strings but will have the 'f' prefix removed OR (b) substrings + # that were not f-strings but will now become f-strings because of + # redundant use of the 'f' prefix (i.e. none of the substrings + # contain f-expressions but one or more of them had the 'f' prefix + # anyway; in which case, we will prepend 'f' to _all_ substrings). + # + # There is probably a better way to accomplish what is being done + # here... + # + # If this substring is an f-string, we _could_ remove the 'f' + # prefix, and the current custom split did NOT originally use a + # prefix... if ( - # Are we allowed to try to drop a pointless 'f' prefix? - drop_pointless_f_prefix - # If we are, will we be successful? - and next_value != self._normalize_f_string(next_value, prefix) + next_value != self._normalize_f_string(next_value, prefix) + and use_custom_breakpoints + and not csplit.has_prefix ): - # If the current custom split did NOT originally use a prefix, - # then `csplit.break_idx` will be off by one after removing + # Then `csplit.break_idx` will be off by one after removing # the 'f' prefix. - break_idx = ( - break_idx + 1 - if use_custom_breakpoints and not csplit.has_prefix - else break_idx - ) + break_idx += 1 next_value = rest_value[:break_idx] + QUOTE + + if drop_pointless_f_prefix: next_value = self._normalize_f_string(next_value, prefix) # --- Construct `next_leaf` diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index d46f96a82d0..83cae614e69 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -455,6 +455,45 @@ async def foo(self): ) assert str(suffix_arr) in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" assert str(suffix_arr) not in "['$', 'angaroo$', 'angrykangaroo$', 'aroo$', 'garoo$', 'grykangaroo$', 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$', 'rykangaroo$', 'ykangaroo$']" +message = ( + f"1. Go to Google Developers Console and log in with your Google account." + "(https://console.developers.google.com/)" + "2. You should be prompted to create a new project (name does not matter)." + "3. Click on Enable APIs and Services at the top." + "4. In the list of APIs choose or search for YouTube Data API v3 and " + "click on it. Choose Enable." + "5. Click on Credentials on the left navigation bar." + "6. Click on Create Credential at the top." + '7. At the top click the link for "API key".' + "8. No application restrictions are needed. Click Create at the bottom." + "9. You now have a key to add to `{prefix}set api youtube api_key`" +) +message = ( + f"1. Go to Google Developers Console and log in with your Google account." + "(https://console.developers.google.com/)" + "2. You should be prompted to create a new project (name does not matter)." + f"3. Click on Enable APIs and Services at the top." + "4. In the list of APIs choose or search for YouTube Data API v3 and " + "click on it. Choose Enable." + f"5. Click on Credentials on the left navigation bar." + "6. Click on Create Credential at the top." + '7. At the top click the link for "API key".' + "8. No application restrictions are needed. Click Create at the bottom." + "9. You now have a key to add to `{prefix}set api youtube api_key`" +) +message = ( + f"1. Go to Google Developers Console and log in with your Google account." + "(https://console.developers.google.com/)" + "2. You should be prompted to create a new project (name does not matter)." + f"3. Click on Enable APIs and Services at the top." + "4. In the list of APIs choose or search for YouTube Data API v3 and " + "click on it. Choose Enable." + f"5. Click on Credentials on the left navigation bar." + "6. Click on Create Credential at the top." + '7. At the top click the link for "API key".' + "8. No application restrictions are needed. Click Create at the bottom." + f"9. You now have a key to add to `{prefix}set api youtube api_key`" +) # output @@ -1022,3 +1061,42 @@ async def foo(self): " 'kangaroo$', 'ngaroo$', 'ngrykangaroo$', 'o$', 'oo$', 'roo$'," " 'rykangaroo$', 'ykangaroo$']" ) +message = ( + f"1. Go to Google Developers Console and log in with your Google account." + f"(https://console.developers.google.com/)" + f"2. You should be prompted to create a new project (name does not matter)." + f"3. Click on Enable APIs and Services at the top." + f"4. In the list of APIs choose or search for YouTube Data API v3 and " + f"click on it. Choose Enable." + f"5. Click on Credentials on the left navigation bar." + f"6. Click on Create Credential at the top." + f'7. At the top click the link for "API key".' + f"8. No application restrictions are needed. Click Create at the bottom." + f"9. You now have a key to add to `{{prefix}}set api youtube api_key`" +) +message = ( + f"1. Go to Google Developers Console and log in with your Google account." + f"(https://console.developers.google.com/)" + f"2. You should be prompted to create a new project (name does not matter)." + f"3. Click on Enable APIs and Services at the top." + f"4. In the list of APIs choose or search for YouTube Data API v3 and " + f"click on it. Choose Enable." + f"5. Click on Credentials on the left navigation bar." + f"6. Click on Create Credential at the top." + f'7. At the top click the link for "API key".' + f"8. No application restrictions are needed. Click Create at the bottom." + f"9. You now have a key to add to `{{prefix}}set api youtube api_key`" +) +message = ( + "1. Go to Google Developers Console and log in with your Google account." + "(https://console.developers.google.com/)" + "2. You should be prompted to create a new project (name does not matter)." + "3. Click on Enable APIs and Services at the top." + "4. In the list of APIs choose or search for YouTube Data API v3 and " + "click on it. Choose Enable." + "5. Click on Credentials on the left navigation bar." + "6. Click on Create Credential at the top." + '7. At the top click the link for "API key".' + "8. No application restrictions are needed. Click Create at the bottom." + f"9. You now have a key to add to `{prefix}set api youtube api_key`" +) From c1c2418368cfcaa4f49edd7ec599fa45cce2d47d Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 7 Jun 2021 08:05:08 -0700 Subject: [PATCH 282/680] [primer] Enable everything (#2288) See if we pass all our repos with experimental string processing enabled. Django probably needed: - Ignores >= 3.8 only We could support PEP440 version specifiers, but that would introduce the packaging module as a dependency that I'd like to avoid ... Or I could implement a poor persons version or vendor Commit history before merge: * [primer] Enable everything * Add exclude extend to django CLI args for primer * Change default timeout to from 5 to 10 mins for a primer project * Skip string normalization for Django * Limit Django to >= 3.8 due to := operator --- src/black_primer/lib.py | 2 +- src/black_primer/primer.json | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 384c0ad6cea..df67602b6a5 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -39,7 +39,7 @@ class Results(NamedTuple): async def _gen_check_output( cmd: Sequence[str], - timeout: float = 300, + timeout: float = 600, env: Optional[Dict[str, str]] = None, cwd: Optional[Path] = None, ) -> Tuple[bytes, bytes]: diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 034372609a1..f1035a819ce 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -30,13 +30,16 @@ "py_versions": ["all"] }, "django": { - "disabled_reason": "black --check --diff returned 123 on tests_syntax_error.py", - "disabled": true, - "cli_arguments": ["--experimental-string-processing"], + "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": ["all"] + "py_versions": ["3.8", "3.9"] }, "flake8-bugbear": { "cli_arguments": ["--experimental-string-processing"], @@ -53,8 +56,6 @@ "py_versions": ["all"] }, "pandas": { - "disabled_reason": "black-primer runs failing on Pandas - #2193", - "disabled": true, "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, "git_clone_url": "https://github.com/pandas-dev/pandas.git", From 40fae18134916b8499bd992d8bef4ae23bcd2986 Mon Sep 17 00:00:00 2001 From: Sergey Vartanov Date: Wed, 9 Jun 2021 00:37:34 +0300 Subject: [PATCH 283/680] Possible fix for issue with indentation and fmt: skip (#2281) Not sure the fix is right. Here is what I found: issue is connected with line first.prefix = prefix[comment.consumed :] in `comments.py`. `first.prefix` is a prefix of the line, that ends with `# fmt: skip`, but `comment.consumed` is the length of the `" # fmt: skip"` string. If prefix length is greater than 14, `first.prefix` will grow every time we apply formatting. Fixes #2254 --- CHANGES.md | 1 + src/black/comments.py | 5 ++++- tests/data/fmtskip6.py | 13 +++++++++++++ tests/test_format.py | 1 + 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/data/fmtskip6.py diff --git a/CHANGES.md b/CHANGES.md index 02b3fdf75d5..2d2b3b4cf49 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### _Black_ +- Fix failure caused by `fmt: skip` and indentation (#2281) - Account for += assignment when deciding whether to split string (#2312) - Correct max string length calculation when there are string operators (#2292) - Fixed option usage when using the `--code` flag (#2259) diff --git a/src/black/comments.py b/src/black/comments.py index 415e391b2a7..c7513c21ef5 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -159,7 +159,10 @@ def convert_one_fmt_off_pair(node: Node) -> bool: first = ignored_nodes[0] # Can be a container node with the `leaf`. parent = first.parent prefix = first.prefix - first.prefix = prefix[comment.consumed :] + if comment.value in FMT_OFF: + first.prefix = prefix[comment.consumed :] + if comment.value in FMT_SKIP: + first.prefix = "" hidden_value = "".join(str(n) for n in ignored_nodes) if comment.value in FMT_OFF: hidden_value = comment.value + "\n" + hidden_value diff --git a/tests/data/fmtskip6.py b/tests/data/fmtskip6.py new file mode 100644 index 00000000000..0a779fcee00 --- /dev/null +++ b/tests/data/fmtskip6.py @@ -0,0 +1,13 @@ +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip + +# output + +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip diff --git a/tests/test_format.py b/tests/test_format.py index 5c78afe0ba6..fc9678ad27c 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -41,6 +41,7 @@ "fmtskip3", "fmtskip4", "fmtskip5", + "fmtskip6", "fstring", "function", "function2", From a9eab85f226df3b3070aca122d089dbd62b42b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Wed, 9 Jun 2021 00:57:23 +0300 Subject: [PATCH 284/680] Mention comment non-processing in documentation (#2306) This commit adds a short section discussing the non-processing of docstrings besides spacing improvements, mentions comment moving and links to the AST equivalence discussion. I also added a simple spacing test for good measure. Commit history before merge: * Mention comment non-processing in documentation, add spacing test * Mention special cases for comment spacing * Add all special cases, improve wording --- docs/the_black_code_style/current_style.md | 10 ++++++++++ tests/data/comments2.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 8c3a30270d1..aa2dd881a9e 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -200,6 +200,16 @@ following field or method. This conforms to _Black_ won't insert empty lines after function docstrings unless that empty line is required due to an inner function starting immediately after. +### Comments + +_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. + ### Trailing commas _Black_ will add trailing commas to expressions that are split by comma where each diff --git a/tests/data/comments2.py b/tests/data/comments2.py index 221cb3fe143..4eea013151a 100644 --- a/tests/data/comments2.py +++ b/tests/data/comments2.py @@ -159,7 +159,7 @@ def _init_host(self, parsed) -> None: ####################### -instruction() +instruction()#comment with bad spacing # END COMMENTS # MORE END COMMENTS @@ -336,7 +336,7 @@ def _init_host(self, parsed) -> None: ####################### -instruction() +instruction() # comment with bad spacing # END COMMENTS # MORE END COMMENTS From 00e7e12a3a412ea386806d5d4eeaed345e912940 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 8 Jun 2021 20:46:09 -0400 Subject: [PATCH 285/680] Regression fix: leave R prefixes capitalization alone (#2285) `black.strings.get_string_prefix` used to lowercase the extracted prefix before returning it. This is wrong because 1) it ignores the fact we should leave R prefixes alone because of MagicPython, and 2) there is dedicated prefix casing handling code that fixes issue 1. `.lower` is too naive. This was originally fixed in 20.8b0, but was reintroduced since 21.4b0. I also added proper prefix normalization for docstrings by using the `black.strings.normalize_string_prefix` helper. Some more test strings were added to make sure strings with capitalized prefixes aren't treated differently (actually happened with my original patch, Jelle had to point it out to me). --- CHANGES.md | 1 + src/black/linegen.py | 5 +-- src/black/strings.py | 2 +- src/black/trans.py | 10 +++--- tests/data/long_strings__regression.py | 42 ++++++++++++++++++++++++++ tests/data/string_prefixes.py | 21 +++++++++++++ 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2d2b3b4cf49..9c2939e1b1b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Added `--required-version` option to require a specific version to be running (#2300) - Fix incorrect custom breakpoint indices when string group contains fake f-strings (#2311) +- Fix regression where `R` prefixes would be lowercased for docstrings (#2285) ## 21.5b2 diff --git a/src/black/linegen.py b/src/black/linegen.py index 7949654b40e..3b811f02b7c 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -226,8 +226,9 @@ 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. - prefix = get_string_prefix(leaf.value) - docstring = leaf.value[len(prefix) :] # Remove the prefix + docstring = normalize_string_prefix(leaf.value, self.remove_u_prefix) + prefix = get_string_prefix(docstring) + docstring = docstring[len(prefix) :] # Remove the prefix quote_char = docstring[0] # A natural way to remove the outer quotes is to do: # docstring = docstring.strip(quote_char) diff --git a/src/black/strings.py b/src/black/strings.py index 5b443ddebc9..80f588f5119 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -87,7 +87,7 @@ def get_string_prefix(string: str) -> str: prefix = "" prefix_idx = 0 while string[prefix_idx] in STRING_PREFIX_CHARS: - prefix += string[prefix_idx].lower() + prefix += string[prefix_idx] prefix_idx += 1 return prefix diff --git a/src/black/trans.py b/src/black/trans.py index 4fb2c4d0144..ca620f6b2a5 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -411,7 +411,7 @@ def make_naked(string: str, string_prefix: str) -> str: and is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING ): - prefix = get_string_prefix(LL[next_str_idx].value) + prefix = get_string_prefix(LL[next_str_idx].value).lower() next_str_idx += 1 # The next loop merges the string group. The final string will be @@ -431,7 +431,7 @@ def make_naked(string: str, string_prefix: str) -> str: num_of_strings += 1 SS = LL[next_str_idx].value - next_prefix = get_string_prefix(SS) + next_prefix = get_string_prefix(SS).lower() # If this is an f-string group but this substring is not prefixed # with 'f'... @@ -541,7 +541,7 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: return TErr("StringMerger does NOT merge multiline strings.") num_of_strings += 1 - prefix = get_string_prefix(leaf.value) + prefix = get_string_prefix(leaf.value).lower() if "r" in prefix: return TErr("StringMerger does NOT merge raw strings.") @@ -1036,7 +1036,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: is_valid_index = is_valid_index_factory(LL) insert_str_child = insert_str_child_factory(LL[string_idx]) - prefix = get_string_prefix(LL[string_idx].value) + prefix = get_string_prefix(LL[string_idx].value).lower() # We MAY choose to drop the 'f' prefix from substrings that don't # contain any f-expressions, but ONLY if the original f-string @@ -1290,7 +1290,7 @@ def fexpr_slices() -> Iterator[Tuple[Index, Index]]: yield from _fexpr_slices - is_fstring = "f" in get_string_prefix(string) + is_fstring = "f" in get_string_prefix(string).lower() def breaks_fstring_expression(i: Index) -> bool: """ diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 83cae614e69..e4234b2f97c 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -495,6 +495,26 @@ async def foo(self): f"9. You now have a key to add to `{prefix}set api youtube api_key`" ) +# It shouldn't matter if the string prefixes are capitalized. +temp_msg = ( + F"{F'{humanize_number(pos)}.': <{pound_len+2}} " + F"{balance: <{bal_len + 5}} " + F"<<{author.display_name}>>\n" +) + +fstring = ( + F"We have to remember to escape {braces}." + " Like {these}." + F" But not {this}." +) + +welcome_to_programming = R"hello," R" world!" + +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." + +x = F"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}." + + # output @@ -1100,3 +1120,25 @@ async def foo(self): "8. No application restrictions are needed. Click Create at the bottom." f"9. You now have a key to add to `{prefix}set api youtube api_key`" ) + +# It shouldn't matter if the string prefixes are capitalized. +temp_msg = ( + f"{F'{humanize_number(pos)}.': <{pound_len+2}} " + f"{balance: <{bal_len + 5}} " + f"<<{author.display_name}>>\n" +) + +fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}." + +welcome_to_programming = R"hello," R" world!" + +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" + f" to have the 'f' whereas others do not. This {line}, for example, needs one." +) + +x = ( + "This is a long string which contains an f-expr that should not split" + f" {{{[i for i in range(5)]}}}." +) diff --git a/tests/data/string_prefixes.py b/tests/data/string_prefixes.py index 0ca3686a2b6..9ddc2b540fc 100644 --- a/tests/data/string_prefixes.py +++ b/tests/data/string_prefixes.py @@ -6,6 +6,17 @@ r"hello" fR"hello" + +def docstring_singleline(): + R"""2020 was one hell of a year. The good news is that we were able to""" + + +def docstring_multiline(): + R""" + clear out all of the issues opened in that time :p + """ + + # output @@ -16,3 +27,13 @@ b"hello" r"hello" fR"hello" + + +def docstring_singleline(): + R"""2020 was one hell of a year. The good news is that we were able to""" + + +def docstring_multiline(): + R""" + clear out all of the issues opened in that time :p + """ From 229498e531b26f93c482115b1ccdb16b70bad620 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:01:07 +0200 Subject: [PATCH 286/680] Fix flake8 configuration by switching from extend-ignore to ignore (#2320) --- .flake8 | 2 +- src/black/output.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 4866133a6a8..ae11a13347c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -extend-ignore = E203, E266, E501 +ignore = E203, E266, E501, W503 # 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/src/black/output.py b/src/black/output.py index 6831524090d..c253c85e90e 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -45,7 +45,8 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str: a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 ): # Work around https://bugs.python.org/issue2142 - # See https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html + # See: + # https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html if line[-1] == "\n": diff_lines.append(line) else: From 62402a32618bc62ae90cfcdc3d47c7ad20e60e10 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Wed, 9 Jun 2021 21:29:32 +0200 Subject: [PATCH 287/680] Support named escapes (`\N{...}`) in string processing (#2319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Hildén Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 + src/black/trans.py | 91 +++++++++++++++++--------- tests/data/long_strings.py | 72 ++++++++++++++++++++ tests/data/long_strings__regression.py | 8 +++ 4 files changed, 143 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9c2939e1b1b..01c02fe2b70 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,8 @@ - Fix incorrect custom breakpoint indices when string group contains fake f-strings (#2311) - Fix regression where `R` prefixes would be lowercased for docstrings (#2285) +- Fix handling of named escapes (`\N{...}`) when `--experimental-string-processing` is + used (#2319) ## 21.5b2 diff --git a/src/black/trans.py b/src/black/trans.py index ca620f6b2a5..023dcd3618a 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -15,6 +15,7 @@ List, Optional, Sequence, + Set, Tuple, TypeVar, Union, @@ -1243,6 +1244,61 @@ def more_splits_should_be_made() -> bool: last_line.comments = line.comments.copy() yield Ok(last_line) + def _iter_nameescape_slices(self, string: str) -> Iterator[Tuple[Index, Index]]: + """ + Yields: + All ranges of @string which, if @string were to be split there, + would result in the splitting of an \\N{...} expression (which is NOT + allowed). + """ + # True - the previous backslash was unescaped + # False - the previous backslash was escaped *or* there was no backslash + previous_was_unescaped_backslash = False + it = iter(enumerate(string)) + for idx, c in it: + if c == "\\": + previous_was_unescaped_backslash = not previous_was_unescaped_backslash + continue + if not previous_was_unescaped_backslash or c != "N": + previous_was_unescaped_backslash = False + continue + previous_was_unescaped_backslash = False + + begin = idx - 1 # the position of backslash before \N{...} + for idx, c in it: + if c == "}": + end = idx + break + else: + # malformed nameescape expression? + # should have been detected by AST parsing earlier... + raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!") + yield begin, end + + def _iter_fexpr_slices(self, string: str) -> Iterator[Tuple[Index, Index]]: + """ + Yields: + All ranges of @string which, if @string were to be split there, + would result in the splitting of an f-expression (which is NOT + allowed). + """ + if "f" not in get_string_prefix(string).lower(): + return + + for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE): + yield match.span() + + def _get_illegal_split_indices(self, string: str) -> Set[Index]: + illegal_indices: Set[Index] = set() + iterators = [ + self._iter_fexpr_slices(string), + self._iter_nameescape_slices(string), + ] + for it in iterators: + for begin, end in it: + illegal_indices.update(range(begin, end + 1)) + return illegal_indices + def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]: """ This method contains the algorithm that StringSplitter uses to @@ -1272,40 +1328,15 @@ def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]: assert is_valid_index(max_break_idx) assert_is_leaf_string(string) - _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None - - def fexpr_slices() -> Iterator[Tuple[Index, Index]]: - """ - Yields: - All ranges of @string which, if @string were to be split there, - would result in the splitting of an f-expression (which is NOT - allowed). - """ - nonlocal _fexpr_slices - - if _fexpr_slices is None: - _fexpr_slices = [] - for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE): - _fexpr_slices.append(match.span()) - - yield from _fexpr_slices - - is_fstring = "f" in get_string_prefix(string).lower() + _illegal_split_indices = self._get_illegal_split_indices(string) - def breaks_fstring_expression(i: Index) -> bool: + def breaks_unsplittable_expression(i: Index) -> bool: """ Returns: True iff returning @i would result in the splitting of an - f-expression (which is NOT allowed). + unsplittable expression (which is NOT allowed). """ - if not is_fstring: - return False - - for (start, end) in fexpr_slices(): - if start <= i < end: - return True - - return False + return i in _illegal_split_indices def passes_all_checks(i: Index) -> bool: """ @@ -1329,7 +1360,7 @@ def passes_all_checks(i: Index) -> bool: is_space and is_not_escaped and is_big_enough - and not breaks_fstring_expression(i) + and not breaks_unsplittable_expression(i) ) # First, we check all indices BELOW @max_break_idx. diff --git a/tests/data/long_strings.py b/tests/data/long_strings.py index 151396b5239..430f760cf0b 100644 --- a/tests/data/long_strings.py +++ b/tests/data/long_strings.py @@ -207,6 +207,38 @@ def foo(): " of it." ) +string_with_nameescape = ( + "........................................................................ \N{LAO KO LA}" +) + +string_with_nameescape = ( + "........................................................................... \N{LAO KO LA}" +) + +string_with_nameescape = ( + "............................................................................ \N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + "...................................................................... \\\N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + "......................................................................... \\\N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + ".......................................................................... \\\N{LAO KO LA}" +) + +string_with_escaped_nameescape = ( + "........................................................................ \\N{LAO KO LA}" +) + +string_with_escaped_nameescape = ( + "........................................................................... \\N{LAO KO LA}" +) + # output @@ -587,3 +619,43 @@ def foo(): "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check " of it." ) + +string_with_nameescape = ( + "........................................................................" + " \N{LAO KO LA}" +) + +string_with_nameescape = ( + "..........................................................................." + " \N{LAO KO LA}" +) + +string_with_nameescape = ( + "............................................................................" + " \N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + "......................................................................" + " \\\N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + "........................................................................." + " \\\N{LAO KO LA}" +) + +string_with_nameescape_and_escaped_backslash = ( + ".........................................................................." + " \\\N{LAO KO LA}" +) + +string_with_escaped_nameescape = ( + "........................................................................ \\N{LAO" + " KO LA}" +) + +string_with_escaped_nameescape = ( + "..........................................................................." + " \\N{LAO KO LA}" +) diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index e4234b2f97c..61c28d376ef 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -514,6 +514,10 @@ async def foo(self): x = F"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}." +x = ( + "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}" +) + # output @@ -1142,3 +1146,7 @@ async def foo(self): "This is a long string which contains an f-expr that should not split" f" {{{[i for i in range(5)]}}}." ) + +x = ( + "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}" +) From 2c5150c7c679b939ba05de1050a2b839cd4f8c96 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Wed, 9 Jun 2021 22:33:53 +0200 Subject: [PATCH 288/680] Don't run Docker workflow on push to forks (#2324) --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 788c505ee4a..419ec74ba3c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,6 +9,7 @@ on: jobs: docker: + if: github.repository == 'psf/black' runs-on: ubuntu-latest steps: - name: Checkout From 77021f0fb203969d0c2437f1f9dc38f08a1b69fb Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Wed, 9 Jun 2021 22:33:59 +0200 Subject: [PATCH 289/680] Add coverage files to gitignore (#2323) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f487d96cb3d..da7328c2865 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .venv .coverage +.coverage.* _build .DS_Store .vscode From 93c10bf9ebccf8d7cc686b0b9579f2e5e41c5328 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 10 Jun 2021 15:25:47 -0700 Subject: [PATCH 290/680] Update CHANGES.md for 21.6b0 release (#2325) --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 01c02fe2b70..caaf1c3d7ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.6b0 ### _Black_ From 05b54b84327cfd1089cc4d630c7c65880252bcb4 Mon Sep 17 00:00:00 2001 From: Ryan McPartlan Date: Thu, 10 Jun 2021 20:45:43 -0400 Subject: [PATCH 291/680] Fix incorrect document referance (#2326) --- .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 2527c4d42ab..207766b0b3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ # Note: don't use this config for your own repositories. Instead, see -# "Version control integration" in README.md. +# "Version control integration" in docs/integrations/source_version_control.md exclude: ^(src/blib2to3/|profiling/|tests/data/) repos: - repo: local From aa31a117b16ed849710cf13cea5c064839beb29e Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Thu, 10 Jun 2021 21:06:50 -0700 Subject: [PATCH 292/680] Add STDIN test to primer (#2315) * Add STDIN test to primer - Check that out STDIN black support stays working - Add asyncio.subprocess STDIN pip via communicate - We just check we format python code from primer's `lib.py` Fixes #2310 --- CHANGES.md | 6 +++++ src/black_primer/lib.py | 45 +++++++++++++++++++++++++++--------- src/black_primer/primer.json | 7 ++++++ tests/test_primer.py | 17 ++++++++++---- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index caaf1c3d7ad..d22062ad542 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +### _Black_ + +- Add primer support and test for code piped into black via STDIN (#2315) + ## 21.6b0 ### _Black_ diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index df67602b6a5..784134bf652 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -42,16 +42,18 @@ async def _gen_check_output( timeout: float = 600, 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(), timeout) + (stdout, stderr) = await asyncio.wait_for(process.communicate(stdin), timeout) except asyncio.TimeoutError: process.kill() await process.wait() @@ -112,20 +114,35 @@ def analyze_results(project_count: int, results: Results) -> int: async def black_run( - repo_path: Path, + 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(project_config["cli_arguments"]) cmd.append("--check") - if no_diff: - cmd.append(".") + 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() else: - cmd.extend(["--diff", "."]) + cmd.append(".") with TemporaryDirectory() as tmp_path: # Prevent reading top-level user configs by manipulating environment variables @@ -135,8 +152,11 @@ async def black_run( "USERPROFILE": tmp_path, # Windows (changes `Path.home()` output) } + cwd_path = repo_path.parent if stdin_test else repo_path try: - _stdout, _stderr = await _gen_check_output(cmd, cwd=repo_path, env=env) + _stdout, _stderr = await _gen_check_output( + cmd, cwd=cwd_path, env=env, stdin=stdin + ) except asyncio.TimeoutError: results.stats["failed"] += 1 LOG.error(f"Running black for {repo_path} timed out ({cmd})") @@ -289,12 +309,15 @@ async def project_runner( LOG.debug(f"Skipping {project_name} as it's configured as a long checkout") continue - repo_path = await git_checkout_or_rebase(work_path, project_config, rebase) - if not repo_path: - continue - await black_run(repo_path, project_config, results, no_diff) + 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: + if not keep and not stdin_project: LOG.debug(f"Removing {repo_path}") rmtree_partial = partial( rmtree, path=repo_path, onerror=handle_PermissionError diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index f1035a819ce..90643987942 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -1,6 +1,13 @@ { "configuration_format_version": 20200509, "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, diff --git a/tests/test_primer.py b/tests/test_primer.py index 8bfecd61a57..3c1ec2f929f 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -106,13 +106,16 @@ def test_analyze_results(self) -> None: 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(repo_path, project_config, results)) + 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) @@ -120,7 +123,9 @@ def test_black_run(self) -> None: 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(repo_path, project_config, results)) + 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) @@ -128,13 +133,17 @@ def test_black_run(self) -> None: 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(repo_path, project_config, results)) + 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(repo_path, project_config, results)) + loop.run_until_complete( + lib.black_run(project_name, repo_path, project_config, results) + ) self.assertEqual(2, results.stats["failed"]) @event_loop() From 4a007a881fa9d7757820657056aa376c05e49e9e Mon Sep 17 00:00:00 2001 From: Austin Glaser Date: Sat, 12 Jun 2021 12:52:49 -0700 Subject: [PATCH 293/680] Find pyproject from vim relative to current file (#1871) Commit history before merge: * Find pyproject from vim relative to current file * Merge remote-tracking branch 'upstream/main' into find-pyproject-vim * Finish and fix this patch (thanks Matt Wozniski!) Both the existing code and the proposed code are broken. The vim.eval() call (whether it's vim.eval("@%") or vim.eval("fnamemodify(getcwd(), ':t')) returns a string, and it passes that string to find_pyproject_toml, which expects a sequence of strings, not a single string, and - since a string is a sequence of single character strings - it gets turned into a list of ridiculous paths. I tested with a file called foo.py, and added a print(path_srcs) into find_project_root, which printed out: [ PosixPath('/home/matt/f'), PosixPath('/home/matt/o'), PosixPath('/home/matt/o'), PosixPath('/home/matt'), PosixPath('/home/matt/p'), PosixPath('/home/matt/y') ] This does work for an unnamed buffer, too - we wind up calling black.find_pyproject_toml(("",)), and that winds up prepending the working directory to any relative paths, so "" just gets turned into the current working directory. Note that find_pyproject_toml needs to be passed a 1-tuple, not a list, because it requires something hashable (thanks to functools.lru_cache being used) Co-authored-by: Matt Wozniski * I forgot the CHANGELOG entry ... again * I'm really bad at dealing with merge conflicts sometimes * Be more correct describing search behaviour Co-authored-by: Austin Glaser Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Matt Wozniski --- CHANGES.md | 5 +++++ autoload/black.vim | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d22062ad542..a5205ba7a06 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,11 @@ - Fix handling of named escapes (`\N{...}`) when `--experimental-string-processing` is used (#2319) +### Integrations + +- The vim plugin now searches upwards from the directory containing the current buffer + instead of the current working directory for pyproject.toml. (#1871) + ## 21.5b2 ### _Black_ diff --git a/autoload/black.vim b/autoload/black.vim index f0357b07123..0d93aa899d0 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -139,7 +139,8 @@ def Black(): print(f'Reformatted in {time.time() - start:.4f}s.') def get_configs(): - path_pyproject_toml = black.find_pyproject_toml(vim.eval("fnamemodify(getcwd(), ':t')")) + filename = vim.eval("@%") + path_pyproject_toml = black.find_pyproject_toml((filename,)) if path_pyproject_toml: toml_config = black.parse_pyproject_toml(path_pyproject_toml) else: From 52384bf0a3fe4b0d0a11daad6ef90b8cd948a773 Mon Sep 17 00:00:00 2001 From: Bartosz Telenczuk Date: Sat, 12 Jun 2021 21:55:10 +0200 Subject: [PATCH 294/680] Vim plugin fix string normalization option (#1869) This commit fixes parsing of the skip-string-normalization option in vim plugin. Originally, the plugin read the string-normalization option, which does not exist in help (--help) and it's not respected by black on command line. Commit history before merge: * fix string normalization option in vim plugin * fix string normalization option in vim plugin * Finish and fix patch (thanks Matt Wozniski!) FYI: this is totally the work and the comments below of Matt (AKA godlygeek) This fixes two entirely different problems related to how pyproject.toml files are handled by the vim plugin. === Problem #1 === The plugin fails to properly read boolean values from pyproject.toml. For instance, if you create this pyproject.toml: ``` [tool.black] quiet = true ``` the Black CLI is happy with it and runs without any messages, but the :Black command provided by this plugin fails with: ``` Traceback (most recent call last): File "", line 1, in File "", line 102, in Black File "", line 150, in get_configs File "", line 150, in File "/usr/lib/python3.6/distutils/util.py", line 311, in strtobool val = val.lower() AttributeError: 'bool' object has no attribute 'lower' ``` That's because the value returned by the toml.load() is already a bool, but the vim plugin incorrectly tries to convert it from a str to a bool. The value returned by toml_config.get() was always being passed to flag.cast(), which is a function that either converts a string to an int or a string to a bool, depending on the flag. vim.eval() returns integers and strings all as str, which is why we need the cast, but that's the wrong thing to do for values that came from toml.load(). We should be applying the cast only to the return from vim.eval() (since we know it always gives us a string), rather than casting the value that toml.load() found - which is already the right type. === Problem #2 === The vim plugin fails to take the value for skip_string_normalization from pyproject.toml. That's because it looks for a string_normalization key instead of a skip_string_normalization key, thanks to this line saying the name of the flag is string_normalization: black/autoload/black.vim (line 25 in 05b54b8) ``` Flag(name="string_normalization", cast=strtobool), ``` and this dictcomp looking up each flag's name in the config dict: black/autoload/black.vim (lines 148 to 151 in 05b54b8) ``` return { flag.var_name: flag.cast(toml_config.get(flag.name, vim.eval(flag.vim_rc_name))) for flag in FLAGS } ``` For the second issue, I think I'd do a slightly different patch. I'd keep the change to invert this flag's meaning and change its name that this PR proposes, but I'd also change the handling of the g:black_skip_string_normalization and g:black_string_normalization variables to make it clear that g:black_skip_string_normalization is the expected name, and g:black_string_normalization is only checked when the expected name is unset, for backwards compatibility. My proposed behavior is to check if g:black_skip_string_normalization is defined and to define it if not, using the inverse of g:black_string_normalization if that is set, and otherwise to the default of 0. The Python code in autoload/black.vim runs later, and will use the value of g:black_skip_string_normalization (and ignore g:black_string_normalization; it will only be used to set g:black_skip_string_normalization if it wasn't already set). --- Co-authored-by: Matt Wozniski * Fix plugin/black.vim (need to up my vim game) Co-authored-by: Matt Wozniski Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Matt Wozniski Co-authored-by: Matt Wozniski --- CHANGES.md | 7 +++++++ autoload/black.vim | 6 +++--- plugin/black.vim | 8 ++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a5205ba7a06..403723b70cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,13 @@ - The vim plugin now searches upwards from the directory containing the current buffer instead of the current working directory for pyproject.toml. (#1871) +### Integrations + +- The vim plugin now reads the correct string normalization option in pyproject.toml + (#1869) +- The vim plugin no longer crashes Black when there's boolean values in pyproject.toml + (#1869) + ## 21.5b2 ### _Black_ diff --git a/autoload/black.vim b/autoload/black.vim index 0d93aa899d0..29d8f2b88e4 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -22,7 +22,7 @@ class Flag(collections.namedtuple("FlagBase", "name, cast")): FLAGS = [ Flag(name="line_length", cast=int), Flag(name="fast", cast=strtobool), - Flag(name="string_normalization", cast=strtobool), + Flag(name="skip_string_normalization", cast=strtobool), Flag(name="quiet", cast=strtobool), ] @@ -103,7 +103,7 @@ def Black(): configs = get_configs() mode = black.FileMode( line_length=configs["line_length"], - string_normalization=configs["string_normalization"], + string_normalization=not configs["skip_string_normalization"], is_pyi=vim.current.buffer.name.endswith('.pyi'), ) quiet = configs["quiet"] @@ -147,7 +147,7 @@ def get_configs(): toml_config = {} return { - flag.var_name: flag.cast(toml_config.get(flag.name, vim.eval(flag.vim_rc_name))) + flag.var_name: toml_config.get(flag.name, flag.cast(vim.eval(flag.vim_rc_name))) for flag in FLAGS } diff --git a/plugin/black.vim b/plugin/black.vim index b5edb2a6ade..3bdca62e6ad 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -43,11 +43,11 @@ endif if !exists("g:black_linelength") let g:black_linelength = 88 endif -if !exists("g:black_string_normalization") - if exists("g:black_skip_string_normalization") - let g:black_string_normalization = !g:black_skip_string_normalization +if !exists("g:black_skip_string_normalization") + if exists("g:black_string_normalization") + let g:black_skip_string_normalization = !g:black_string_normalization else - let g:black_string_normalization = 1 + let g:black_skip_string_normalization = 0 endif endif if !exists("g:black_quiet") From e2fd914dc172a13c0e6395d2d08efa5a25380381 Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 13 Jun 2021 13:20:50 -0400 Subject: [PATCH 295/680] Fix internal error when FORCE_OPTIONAL_PARENTHESES feature is enabled (#2332) Fixes #2313. --- CHANGES.md | 1 + src/black/linegen.py | 21 +++++++++++++-------- tests/data/long_strings__regression.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 403723b70cc..12e8fccbe4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### _Black_ - Add primer support and test for code piped into black via STDIN (#2315) +- Fix internal error when `FORCE_OPTIONAL_PARENTHESES` feature is enabled (#2332) ## 21.6b0 diff --git a/src/black/linegen.py b/src/black/linegen.py index 3b811f02b7c..76b553a959a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -961,14 +961,19 @@ def run_transformer( result.extend(transform_line(transformed_line, mode=mode, features=features)) - if not ( - transform.__name__ == "rhs" - and line.bracket_tracker.invisible - and not any(bracket.value for bracket in line.bracket_tracker.invisible) - and not line.contains_multiline_strings() - and not result[0].contains_uncollapsable_type_comments() - and not result[0].contains_unsplittable_type_ignore() - and not is_line_short_enough(result[0], line_length=mode.line_length) + if ( + transform.__name__ != "rhs" + or not line.bracket_tracker.invisible + or any(bracket.value for bracket in line.bracket_tracker.invisible) + 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) + # 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 + # call to `append_leaves()` to fail. + or any(leaf.parent is None for leaf in line.leaves) ): return result diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 61c28d376ef..36f323e04d6 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -518,6 +518,12 @@ async def foo(self): "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}" ) +xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxx_xxxx_xxxxx = xxxx.xxxxxx.xxxxxxxxx.xxxxxxxxxxxxxxxxxxxx( + xx_xxxxxx={ + "x3_xxxxxxxx": "xxx3_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxxxxxx_xxxxxx_xxxxxxx", + }, +) + # output @@ -1150,3 +1156,11 @@ async def foo(self): x = ( "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}" ) + +xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxx_xxxx_xxxxx = xxxx.xxxxxx.xxxxxxxxx.xxxxxxxxxxxxxxxxxxxx( + xx_xxxxxx={ + "x3_xxxxxxxx": ( + "xxx3_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxxxxxx_xxxxxx_xxxxxxx" + ), + }, +) From 52f402dcfbeb49d4710b2c8d2b15563b08369fc5 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Sun, 13 Jun 2021 19:22:46 +0200 Subject: [PATCH 296/680] Add EOF and trailing whitespace fixer to pre-commit config (#2330) --- .gitignore | 2 +- .pre-commit-config.yaml | 6 ++++++ docs/Makefile | 2 +- docs/_static/license.svg | 2 +- pyproject.toml | 2 +- src/black/py.typed | 1 - tests/test.toml | 1 - 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index da7328c2865..ab796ce4cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ src/_black_version.py .dmypy.json *.swp .hypothesis/ -venv/ \ No newline at end of file +venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 207766b0b3a..be60ff2e635 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,3 +28,9 @@ repos: rev: v2.2.1 hooks: - id: prettier + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/docs/Makefile b/docs/Makefile index 2e0e5eeaf9d..cb0463c842b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/license.svg b/docs/_static/license.svg index 36bea29f654..25aa18b91af 100644 --- a/docs/_static/license.svg +++ b/docs/_static/license.svg @@ -1 +1 @@ -licenselicenseMITMIT \ No newline at end of file +licenselicenseMITMIT diff --git a/pyproject.toml b/pyproject.toml index 4d6777ef6ec..79060fc422d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,4 +31,4 @@ build-backend = "setuptools.build_meta" optional-tests = [ "no_python2: run when `python2` extra NOT installed", "no_blackd: run when `d` extra NOT installed", -] \ No newline at end of file +] diff --git a/src/black/py.typed b/src/black/py.typed index 8b137891791..e69de29bb2d 100644 --- a/src/black/py.typed +++ b/src/black/py.typed @@ -1 +0,0 @@ - diff --git a/tests/test.toml b/tests/test.toml index 405c00ce2c3..570c2f26b15 100644 --- a/tests/test.toml +++ b/tests/test.toml @@ -7,4 +7,3 @@ line-length = 79 target-version = ["py36", "py37", "py38"] exclude='\.pyi?$' include='\.py?$' - From bd7f49df3e16b52f0b287c28427c306e6c1064c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Sun, 13 Jun 2021 22:27:57 +0300 Subject: [PATCH 297/680] Docs: no space is inserted to empty docstrings (#2249) (#2333) --- 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 aa2dd881a9e..b9ab350cd12 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -275,7 +275,7 @@ for both quotations and the text within, although relative indentation in the te preserved. Superfluous trailing whitespace on each line and unnecessary new lines at the end of the docstring are removed. All leading tabs are converted to spaces, but tabs inside text are preserved. Whitespace leading and trailing one-line docstrings is -removed. The quotations of an empty docstring are separated with one space. +removed. ### Numeric literals From 742ddd156121ea593349dcf864c8aaafeb08b809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Tue, 15 Jun 2021 17:37:59 +0300 Subject: [PATCH 298/680] Chat on Discord instead of Freenode (#2336) Now that we've moved, let's direct our users to Discord in the documentation and readme. --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b4aeee8baa..0b5c7ee39c3 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ You can also take a look at the rest of the contributing docs or talk with the developers: - [Contributing documentation](https://black.readthedocs.io/en/latest/contributing/index.html) -- [IRC channel on Freenode](https://webchat.freenode.net/?channels=%23blackformatter) +- [Chat on Discord](https://discord.gg/RtVdv86PrH) ## Change log diff --git a/docs/index.rst b/docs/index.rst index 44b800cadf8..1515697f556 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -119,7 +119,7 @@ Contents GitHub ↪ PyPI ↪ - IRC ↪ + Chat ↪ Indices and tables ================== From 8d8b3d18059b7f11871eaa376c288f14549dd342 Mon Sep 17 00:00:00 2001 From: Art Chaidarun Date: Thu, 17 Jun 2021 12:32:03 -0400 Subject: [PATCH 299/680] Add Duolingo to list of users (#2341) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b5c7ee39c3..0f1953df341 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, every Datadog Agent Integration, Home Assistant, Zulip. -The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora. +The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo. Are we missing anyone? Let us know. From 3980b4b1767a35c6d99f9231f14d4690d0a270c2 Mon Sep 17 00:00:00 2001 From: SADIK KUZU Date: Tue, 22 Jun 2021 04:28:25 +0300 Subject: [PATCH 300/680] Update pre-commit config (#2331) via `pre-commit autoupdate` ``` Updating https://gitlab.com/pycqa/flake8 ... updating 3.9.0 -> 3.9.2. Updating https://github.com/pre-commit/mirrors-mypy ... updating v0.812 -> v0.902. Updating https://github.com/pre-commit/mirrors-prettier ... updating v2.2.1 -> v2.3.1. ``` Signed-off-by: SADIK KUZU * Add necessary typeshed packages to requirements Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .pre-commit-config.yaml | 12 ++- Pipfile | 6 +- Pipfile.lock | 229 +++++++++++++++++++++++++--------------- 3 files changed, 156 insertions(+), 91 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be60ff2e635..920ae669d71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,21 +13,27 @@ repos: types_or: [python, pyi] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.902 hooks: - id: mypy exclude: ^docs/conf.py + additional_dependencies: + - types-dataclasses >= 0.1.3 + - types-toml >= 0.1.1 + - types-typed-ast >= 1.4.1 + - types-click >= 7.1.2 - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.2.1 + rev: v2.3.1 hooks: - id: prettier + exclude: ^Pipfile\.lock - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 diff --git a/Pipfile b/Pipfile index 789d97c3cbc..f617fdc0a67 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,10 @@ docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipe flake8 = "*" flake8-bugbear = "*" mypy = ">=0.812" +types-click = ">=7.1.2" +types-dataclasses = ">=0.1.3" +types-toml = ">=0.1.1" +types-typed-ast = ">=1.4.1" pre-commit = "*" readme_renderer = "*" MyST-Parser = ">=0.13.7" @@ -33,4 +37,4 @@ toml = ">=0.10.1" typed-ast = "==1.4.2" typing_extensions = {"python_version <" = "3.8","version >=" = "3.7.4"} black = {editable = true,extras = ["d"],path = "."} -dataclasses = {"python_version <" = "3.7","version >" = "0.6"} +dataclasses = {"python_version <" = "3.7","version >" = "0.1.3"} diff --git a/Pipfile.lock b/Pipfile.lock index 4f0b51e7d18..17140de2983 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b450e698cfe4d856397cb9d0695dcd74eb97b1d234de273179d78c0d0efccd0f" + "sha256": "18ff16725509d6d14f44e3a96fe620730c9adfdaa87cbeee74aa397913dedaab" }, "pipfile-spec": 6, "requires": {}, @@ -78,6 +78,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -85,6 +86,7 @@ "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": { @@ -99,15 +101,16 @@ "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" }, "click": { "hashes": [ - "sha256:7d8c289ee437bcb0316820ccee14aefcb056e58d31830ecab8e47eda6540e136", - "sha256:e90e62ced43dc8105fb9a26d62f0d9340b5c8db053a814e25d95c19873ae87db" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], "index": "pypi", - "version": "==8.0.0" + "version": "==8.0.1" }, "dataclasses": { "hashes": [ @@ -115,13 +118,16 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "version": "==0.6" + "python_version <": "3.7", + "version": "==0.6", + "version >": "0.1.3" }, "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], + "markers": "python_version >= '3.5'", "version": "==3.2" }, "multidict": { @@ -164,6 +170,7 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], + "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy-extensions": { @@ -229,6 +236,14 @@ "index": "pypi", "version": "==2021.4.4" }, + "setuptools-scm": { + "hashes": [ + "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", + "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -280,7 +295,9 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "version": "==3.10.0.0" + "python_version <": "3.8", + "version": "==3.10.0.0", + "version >=": "3.7.4" }, "yarl": { "hashes": [ @@ -322,6 +339,7 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], + "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -397,6 +415,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -404,6 +423,7 @@ "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": { @@ -411,6 +431,7 @@ "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" }, "black": { @@ -425,6 +446,7 @@ "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.3.0" }, "certifi": { @@ -493,6 +515,7 @@ "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1", "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.3.0" }, "chardet": { @@ -500,21 +523,23 @@ "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" }, "click": { "hashes": [ - "sha256:7d8c289ee437bcb0316820ccee14aefcb056e58d31830ecab8e47eda6540e136", - "sha256:e90e62ced43dc8105fb9a26d62f0d9340b5c8db053a814e25d95c19873ae87db" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], "index": "pypi", - "version": "==8.0.0" + "version": "==8.0.1" }, "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": { @@ -590,6 +615,7 @@ "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" ], + "markers": "python_version >= '3.6'", "version": "==3.4.7" }, "distlib": { @@ -602,6 +628,7 @@ "docutils": { "hashes": [ "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", + "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], "index": "pypi", @@ -632,16 +659,18 @@ }, "identify": { "hashes": [ - "sha256:92d6ad08eca19ceb17576733759944b94c0761277ddc3acf65e75e57ef190e32", - "sha256:c29e74c3671fe9537715cb695148231d777170ca1498e1c30c675d4ea782afe9" + "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421", + "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306" ], - "version": "==2.2.7" + "markers": "python_full_version >= '3.6.1'", + "version": "==2.2.10" }, "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], + "markers": "python_version >= '3.5'", "version": "==3.2" }, "imagesize": { @@ -649,14 +678,16 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "importlib-metadata": { "hashes": [ - "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786", - "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5" + "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00", + "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139" ], - "version": "==4.4.0" + "markers": "python_version >= '3.6'", + "version": "==4.5.0" }, "jeepney": { "hashes": [ @@ -671,6 +702,7 @@ "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], + "markers": "python_version >= '3.6'", "version": "==3.0.1" }, "keyring": { @@ -678,6 +710,7 @@ "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" ], + "markers": "python_version >= '3.6'", "version": "==23.0.1" }, "markdown-it-py": { @@ -685,6 +718,7 @@ "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3", "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389" ], + "markers": "python_version ~= '3.6'", "version": "==1.1.0" }, "markupsafe": { @@ -724,6 +758,7 @@ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], + "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "mccabe": { @@ -738,6 +773,7 @@ "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c", "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f" ], + "markers": "python_version ~= '3.6'", "version": "==0.2.8" }, "multidict": { @@ -780,35 +816,37 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], + "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "mypy": { "hashes": [ - "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", - "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", - "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", - "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", - "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", - "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", - "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", - "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", - "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", - "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", - "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", - "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", - "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", - "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", - "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", - "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", - "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", - "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", - "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", - "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", - "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", - "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" + "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", + "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", + "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", + "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", + "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", + "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", + "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", + "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", + "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", + "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", + "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", + "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", + "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", + "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", + "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", + "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", + "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", + "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", + "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", + "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", + "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", + "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", + "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" ], "index": "pypi", - "version": "==0.812" + "version": "==0.902" }, "mypy-extensions": { "hashes": [ @@ -820,11 +858,11 @@ }, "myst-parser": { "hashes": [ - "sha256:8d7db76e2f33cd1dc1fe0c76af9f09e5cf19ce2c2e85074bc82f272c0f7c08ce", - "sha256:fc262959a74cdc799d7fa9b30c320c17187485b9a1e8c39e988fc12f3adff63c" + "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603", + "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade" ], "index": "pypi", - "version": "==0.14.0" + "version": "==0.15.1" }, "nodeenv": { "hashes": [ @@ -838,6 +876,7 @@ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pathspec": { @@ -857,17 +896,18 @@ }, "pre-commit": { "hashes": [ - "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712", - "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9" + "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378", + "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4" ], "index": "pypi", - "version": "==2.12.1" + "version": "==2.13.0" }, "pycodestyle": { "hashes": [ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, "pycparser": { @@ -875,6 +915,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -882,6 +923,7 @@ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, "pygments": { @@ -889,6 +931,7 @@ "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], + "markers": "python_version >= '3.5'", "version": "==2.9.0" }, "pyparsing": { @@ -896,6 +939,7 @@ "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" }, "pytz": { @@ -937,6 +981,7 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "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" }, "readme-renderer": { @@ -999,6 +1044,7 @@ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.25.1" }, "requests-toolbelt": { @@ -1028,7 +1074,7 @@ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==6.0.1" }, "six": { @@ -1036,6 +1082,7 @@ "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": { @@ -1047,25 +1094,26 @@ }, "sphinx": { "hashes": [ - "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1", - "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8" + "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c", + "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4" ], "index": "pypi", - "version": "==3.5.4" + "version": "==4.0.2" }, "sphinx-copybutton": { "hashes": [ - "sha256:0e0461df394515284e3907e3f418a0c60ef6ab6c9a27a800c8552772d0a402a2", - "sha256:5125c718e763596e6e52d92e15ee0d6f4800ad3817939be6dee51218870b3e3d" + "sha256:19850e9c1ed09c899f136ce3cfa304cb3b6d2f508528c19d8f512ccc8c66b0d4", + "sha256:3695987d5e98e3b223471aaed8aa7491e03e9bfc48ed655a91446fd5e30b6c25" ], "index": "pypi", - "version": "==0.3.1" + "version": "==0.3.3" }, "sphinxcontrib-applehelp": { "hashes": [ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -1073,6 +1121,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -1080,6 +1129,7 @@ "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" ], + "markers": "python_version >= '3.6'", "version": "==2.0.0" }, "sphinxcontrib-jsmath": { @@ -1087,6 +1137,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-programoutput": { @@ -1102,6 +1153,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -1109,6 +1161,7 @@ "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" ], + "markers": "python_version >= '3.5'", "version": "==1.1.5" }, "toml": { @@ -1121,10 +1174,11 @@ }, "tqdm": { "hashes": [ - "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", - "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" + "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd", + "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2" ], - "version": "==4.61.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.61.1" }, "twine": { "hashes": [ @@ -1134,38 +1188,34 @@ "index": "pypi", "version": "==3.4.1" }, - "typed-ast": { + "types-click": { "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:040897284e4f9466825c3865f708a985a8e7ba4d8e22cb9198ffb7b522160850", + "sha256:4722746f1ec9fd3fc8b1d7fb8c840604dc22f9e32bcc7a31a36d6d85cc2bce24" + ], + "index": "pypi", + "version": "==7.1.2" + }, + "types-dataclasses": { + "hashes": [ + "sha256:7b5f4099fb21c209f2df3a83c2b64308c29955769d610a457244dc0eebe1cafc", + "sha256:c19491cfb981bff9cafd9c113c291a7a54adccc6298ded8ca3de0d7abe211984" + ], + "index": "pypi", + "version": "==0.1.5" + }, + "types-toml": { + "hashes": [ + "sha256:33ebe67bebaec55a123ecbaa2bd98fe588335d8d8dda2c7ac53502ef5a81a79a", + "sha256:d4add39a90993173d49ff0b069edd122c66ad4cf5c01082b590e380ca670ee1a" + ], + "index": "pypi", + "version": "==0.1.3" + }, + "types-typed-ast": { + "hashes": [ + "sha256:014ab2742015d3eaab86ea061c27809c7e76beeb9e134102f2e2ddb34ab05c62", + "sha256:6a47165760321835d160da20a6ac5b9065f23d6dc246162508cad1ef1238afc3" ], "index": "pypi", "version": "==1.4.2" @@ -1177,14 +1227,16 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "version": "==3.10.0.0" + "python_version <": "3.8", + "version": "==3.10.0.0", + "version >=": "3.7.4" }, "urllib3": { "hashes": [ "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" ], - "index": "pypi", + "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.5" }, "virtualenv": { @@ -1192,6 +1244,7 @@ "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4.7" }, "webencodings": { @@ -1249,6 +1302,7 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], + "markers": "python_version >= '3.6'", "version": "==1.6.3" }, "zipp": { @@ -1256,6 +1310,7 @@ "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], + "markers": "python_version >= '3.6'", "version": "==3.4.1" } } From be16cfa0353f33adf0ee5f026d9b46f9ca6ac7ac Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Tue, 22 Jun 2021 18:58:49 +0300 Subject: [PATCH 301/680] Get `click` types from main repo (#2344) Click types have been moved to click repo itself. See pallets/click#1856 I've had some issues with typeshed types being outdated in another project so might be good to avoid that here. Commit history before merge: * Get `click` types from main repo * Fix mypy errors * Require click v8 for type annotations * Update Pipfile --- .pre-commit-config.yaml | 2 +- Pipfile | 3 +-- Pipfile.lock | 10 +--------- tests/test_black.py | 9 ++++++--- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 920ae669d71..79795d1085f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - types-dataclasses >= 0.1.3 - types-toml >= 0.1.1 - types-typed-ast >= 1.4.1 - - types-click >= 7.1.2 + - click >= 8.0.0 - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.3.1 diff --git a/Pipfile b/Pipfile index f617fdc0a67..a27614c94a9 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,6 @@ docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipe flake8 = "*" flake8-bugbear = "*" mypy = ">=0.812" -types-click = ">=7.1.2" types-dataclasses = ">=0.1.3" types-toml = ">=0.1.1" types-typed-ast = ">=1.4.1" @@ -29,7 +28,7 @@ black = {editable = true, extras = ["d"], path = "."} aiohttp = ">=3.6.0" aiohttp-cors = ">=0.4.0" appdirs = "*" -click = ">=7.1.2" +click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" regex = ">=2020.1.8" diff --git a/Pipfile.lock b/Pipfile.lock index 17140de2983..d22131ee859 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "18ff16725509d6d14f44e3a96fe620730c9adfdaa87cbeee74aa397913dedaab" + "sha256": "0c126694d1a1cd1c4dc875a99f330f6b9a5994e51a1f870a98dbacd0185dae53" }, "pipfile-spec": 6, "requires": {}, @@ -1188,14 +1188,6 @@ "index": "pypi", "version": "==3.4.1" }, - "types-click": { - "hashes": [ - "sha256:040897284e4f9466825c3865f708a985a8e7ba4d8e22cb9198ffb7b522160850", - "sha256:4722746f1ec9fd3fc8b1d7fb8c840604dc22f9e32bcc7a31a36d6d85cc2bce24" - ], - "index": "pypi", - "version": "==7.1.2" - }, "types-dataclasses": { "hashes": [ "sha256:7b5f4099fb21c209f2df3a83c2b64308c29955769d610a457244dc0eebe1cafc", diff --git a/tests/test_black.py b/tests/test_black.py index 455cb33e827..5262c0ee44c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -119,6 +119,8 @@ def invokeBlack( 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, @@ -465,6 +467,7 @@ def test_python2_should_fail_without_optional_install(self) -> None: self.assertEqual(result.exit_code, 123) finally: os.unlink(tmp_file) + assert result.stderr_bytes is not None actual = ( result.stderr_bytes.decode() .replace("\n", "") @@ -1892,7 +1895,7 @@ def test_symlink_out_of_root_directory(self) -> None: def test_shhh_click(self) -> None: try: - from click import _unicodefun # type: ignore + from click import _unicodefun except ModuleNotFoundError: self.skipTest("Incompatible Click version") if not hasattr(_unicodefun, "_verify_python3_env"): @@ -1901,14 +1904,14 @@ def test_shhh_click(self) -> None: with patch("locale.getpreferredencoding") as gpe: gpe.return_value = "ASCII" with self.assertRaises(RuntimeError): - _unicodefun._verify_python3_env() + _unicodefun._verify_python3_env() # type: ignore # 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() + _unicodefun._verify_python3_env() # type: ignore except RuntimeError as re: self.fail(f"`patch_click()` failed, exception still raised: {re}") From 017aafea992ca1c6d7af45d3013af7ddb7fda12a Mon Sep 17 00:00:00 2001 From: simaki Date: Thu, 24 Jun 2021 04:11:23 +0900 Subject: [PATCH 302/680] Accept empty stdin (close #2337) (#2346) Commit history before merge: * Accept empty stdin (close #2337) * Update tests/test_black.py * Add changelog * Assert Black reformats an empty string to an empty string (#2337) (#2346) * fix --- CHANGES.md | 1 + src/black/__init__.py | 3 ++- tests/test_black.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 12e8fccbe4e..ea5196e07a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Add primer support and test for code piped into black via STDIN (#2315) - Fix internal error when `FORCE_OPTIONAL_PARENTHESES` feature is enabled (#2332) +- Accept empty stdin (#2346) ## 21.6b0 diff --git a/src/black/__init__.py b/src/black/__init__.py index a985926afa5..8e2123d50cc 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -803,7 +803,8 @@ def format_stdin_to_stdout( ) if write_back == WriteBack.YES: # Make sure there's a newline after the content - dst += "" if dst[-1] == "\n" else "\n" + if dst and dst[-1] != "\n": + dst += "\n" f.write(dst) elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): now = datetime.utcnow() diff --git a/tests/test_black.py b/tests/test_black.py index 5262c0ee44c..42ac119324c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -6,6 +6,7 @@ from contextlib import contextmanager from dataclasses import replace import inspect +import io from io import BytesIO import os from pathlib import Path @@ -1682,6 +1683,20 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None: # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) + def test_reformat_one_with_stdin_empty(self) -> None: + output = io.StringIO() + with patch("io.TextIOWrapper", lambda *args, **kwargs: output): + 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_gitignore_exclude(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" include = re.compile(r"\.pyi?$") From 14072be245b909d2340e13c571e76bf36080cc5a Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sun, 4 Jul 2021 19:23:00 +0100 Subject: [PATCH 303/680] fix typo (#2358) --- 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 d293c40dadf..e866a3cc616 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -3,7 +3,7 @@ You can use _Black_ within a GitHub Actions workflow without setting your own Python environment. Great for enforcing that your code matches the _Black_ code style. -## Compatiblity +## Compatibility 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). From ae56983a5f36d69e2a8cfa762f255a800039b0df Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 8 Jul 2021 21:46:32 -0400 Subject: [PATCH 304/680] Avoid src being marked as optional in help (#2356) --- src/black/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/__init__.py b/src/black/__init__.py index 8e2123d50cc..51384fb08da 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -328,6 +328,7 @@ def validate_regex( exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True ), is_eager=True, + metavar="SRC ...", ) @click.option( "--config", From dd6c674e3a33ac6d20faea751764aeafecdf34c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Sat, 10 Jul 2021 03:09:29 +0300 Subject: [PATCH 305/680] Use setuptools.find_packages in setup (#2363) * Use setuptools.find_packages in setup * Address mypy errors --- setup.py | 4 ++-- src/black_primer/__init__.py | 0 src/black_primer/cli.py | 6 +++--- tests/test_primer.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 src/black_primer/__init__.py diff --git a/setup.py b/setup.py index 5549ae35342..95252eab054 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # Copyright (C) 2020 Łukasz Langa -from setuptools import setup +from setuptools import setup, find_packages import sys import os @@ -62,7 +62,7 @@ def get_long_description() -> str: license="MIT", py_modules=["_black_version"], ext_modules=ext_modules, - packages=["blackd", "black", "blib2to3", "blib2to3.pgen2", "black_primer"], + packages=find_packages(where="src"), package_dir={"": "src"}, package_data={ "blib2to3": ["*.txt"], diff --git a/src/black_primer/__init__.py b/src/black_primer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/black_primer/cli.py b/src/black_primer/cli.py index f0997049d21..8360fc3c703 100644 --- a/src/black_primer/cli.py +++ b/src/black_primer/cli.py @@ -7,7 +7,7 @@ from pathlib import Path from shutil import rmtree, which from tempfile import gettempdir -from typing import Any, Union +from typing import Any, Union, Optional import click @@ -29,8 +29,8 @@ def _handle_debug( - ctx: click.core.Context, - param: Union[click.core.Option, click.core.Parameter], + 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""" diff --git a/tests/test_primer.py b/tests/test_primer.py index 3c1ec2f929f..8dd1212313e 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -189,7 +189,7 @@ 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), td, 2) + lib.process_queue(str(config_path), Path(td), 2) ) self.assertEqual(0, return_val) @@ -210,7 +210,7 @@ def test_async_main(self) -> None: "no_diff": False, } with patch("black_primer.cli.lib.process_queue", return_zero): - return_val = loop.run_until_complete(cli.async_main(**args)) + return_val = loop.run_until_complete(cli.async_main(**args)) # type: ignore self.assertEqual(0, return_val) def test_handle_debug(self) -> None: From 548d6991610ae547b34e6c3058fc8299c9b25658 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sun, 11 Jul 2021 21:03:36 +0100 Subject: [PATCH 306/680] Second run of tox -e py results in a test error for test marked with no_python2 (#2369) Fixes #2367 --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 3ea4da8eac2..a97d18946b3 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,10 @@ envlist = {,ci-}py{36,37,38,39},fuzz [testenv] 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. +# See https://github.com/psf/black/issues/2367. +recreate = True deps = -r{toxinidir}/test_requirements.txt ; parallelization is disabled on CI because pytest-dev/pytest-xdist#620 occurs too frequently From c64fb8cbb121f33f3897b403adffef0ed4314fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 12 Jul 2021 11:56:03 +0200 Subject: [PATCH 307/680] Add LocalStack and Twisted to projects using Black --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f1953df341..7b87a03fc38 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,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, -every Datadog Agent Integration, Home Assistant, Zulip. +Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, and many +more. The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo. From 2946d3b03d499475ed2276e590516df46d01d01a Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Mon, 12 Jul 2021 16:01:38 -0400 Subject: [PATCH 308/680] Switch `toml` TOML library for `tomli` (#2301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toml unfortunately has a lack of maintainership issue right now. It's evident by the fact toml only supports TOML v0.5.0. TOML v1.0.0 has been recently released and right now Black crashes hard on its usage. tomli is a brand new parse only TOML library. It supports TOML v1.0.0. Although TBH we're switching to this one mostly because pip is doing the same. *The upper bound was included at the library maintainer's request. Co-authored-by: Łukasz Langa Co-authored-by: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> --- CHANGES.md | 2 + Pipfile | 2 +- Pipfile.lock | 391 ++++++++++++++++++++++----------------------- setup.py | 2 +- src/black/files.py | 7 +- tests/test.toml | 7 + 6 files changed, 210 insertions(+), 201 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ea5196e07a0..611094f71e9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### _Black_ +- Configuration files using TOML features higher than spec v0.5.0 are now supported + (#2301) - Add primer support and test for code piped into black via STDIN (#2315) - Fix internal error when `FORCE_OPTIONAL_PARENTHESES` feature is enabled (#2332) - Accept empty stdin (#2346) diff --git a/Pipfile b/Pipfile index a27614c94a9..e0d5d1a7969 100644 --- a/Pipfile +++ b/Pipfile @@ -32,7 +32,7 @@ click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" regex = ">=2020.1.8" -toml = ">=0.10.1" +tomli = ">=0.2.6, <2.0.0" typed-ast = "==1.4.2" typing_extensions = {"python_version <" = "3.8","version >=" = "3.7.4"} black = {editable = true,extras = ["d"],path = "."} diff --git a/Pipfile.lock b/Pipfile.lock index d22131ee859..90923038adb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0c126694d1a1cd1c4dc875a99f330f6b9a5994e51a1f870a98dbacd0185dae53" + "sha256": "e6b7c19c10b5fba5ba20296b68049fab625b4c6d61db97b79d83a6dfa36d9f6d" }, "pipfile-spec": 6, "requires": {}, @@ -191,50 +191,50 @@ }, "regex": { "hashes": [ - "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", - "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", - "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", - "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", - "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", - "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", - "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", - "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", - "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", - "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", - "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", - "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", - "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", - "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", - "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", - "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", - "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", - "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", - "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", - "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", - "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", - "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", - "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", - "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", - "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", - "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", - "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", - "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", - "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", - "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", - "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", - "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", - "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", - "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", - "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", - "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", - "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", - "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", - "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", - "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", - "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" + "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", + "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", + "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", + "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", + "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", + "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", + "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", + "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", + "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", + "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", + "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", + "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", + "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", + "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", + "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", + "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", + "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", + "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", + "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", + "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", + "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", + "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", + "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", + "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", + "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", + "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", + "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", + "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", + "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", + "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", + "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", + "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", + "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", + "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", + "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", + "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", + "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", + "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", + "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", + "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", + "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" ], "index": "pypi", - "version": "==2021.4.4" + "version": "==2021.7.6" }, "setuptools-scm": { "hashes": [ @@ -244,13 +244,13 @@ "markers": "python_version >= '3.6'", "version": "==6.0.1" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be", + "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268" ], "index": "pypi", - "version": "==0.10.2" + "version": "==1.0.4" }, "typed-ast": { "hashes": [ @@ -458,57 +458,48 @@ }, "cffi": { "hashes": [ - "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", - "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373", - "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69", - "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f", - "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", - "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05", - "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", - "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", - "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0", - "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", - "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7", - "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f", - "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", - "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", - "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76", - "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", - "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", - "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed", - "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", - "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", - "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", - "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", - "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", - "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", - "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", - "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55", - "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", - "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", - "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", - "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", - "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", - "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", - "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", - "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", - "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", - "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", - "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", - "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", - "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", - "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", - "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", - "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", - "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", - "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc", - "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", - "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", - "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333", - "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", - "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" - ], - "version": "==1.14.5" + "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", + "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", + "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", + "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", + "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", + "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", + "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", + "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", + "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", + "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", + "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", + "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", + "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", + "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", + "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", + "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", + "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", + "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", + "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", + "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", + "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", + "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", + "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:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", + "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", + "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", + "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", + "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" + ], + "version": "==1.14.6" }, "cfgv": { "hashes": [ @@ -659,11 +650,11 @@ }, "identify": { "hashes": [ - "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421", - "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306" + "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839", + "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.10" + "version": "==2.2.11" }, "idna": { "hashes": [ @@ -683,19 +674,19 @@ }, "importlib-metadata": { "hashes": [ - "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00", - "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139" + "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", + "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" ], "markers": "python_version >= '3.6'", - "version": "==4.5.0" + "version": "==4.6.1" }, "jeepney": { "hashes": [ - "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", - "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" + "sha256:1237cd64c8f7ac3aa4b3f332c4d0fb4a8216f39eaa662ec904302d4d77de5a54", + "sha256:71335e7a4e93817982f473f3507bffc2eff7a544119ab9b73e089c8ba1409ba3" ], "markers": "sys_platform == 'linux'", - "version": "==0.6.0" + "version": "==0.7.0" }, "jinja2": { "hashes": [ @@ -821,32 +812,32 @@ }, "mypy": { "hashes": [ - "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", - "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", - "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", - "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", - "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", - "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", - "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", - "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", - "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", - "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", - "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", - "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", - "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", - "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", - "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", - "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", - "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", - "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", - "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", - "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", - "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", - "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", - "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" + "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.902" + "version": "==0.910" }, "mypy-extensions": { "hashes": [ @@ -873,11 +864,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "pathspec": { "hashes": [ @@ -889,10 +880,10 @@ }, "pkginfo": { "hashes": [ - "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", - "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" + "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779", + "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd" ], - "version": "==1.7.0" + "version": "==1.7.1" }, "pre-commit": { "hashes": [ @@ -994,50 +985,50 @@ }, "regex": { "hashes": [ - "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", - "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", - "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", - "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", - "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", - "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", - "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", - "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", - "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", - "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", - "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", - "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", - "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", - "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", - "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", - "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", - "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", - "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", - "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", - "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", - "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", - "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", - "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", - "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", - "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", - "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", - "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", - "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", - "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", - "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", - "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", - "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", - "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", - "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", - "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", - "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", - "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", - "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", - "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", - "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", - "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" + "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", + "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", + "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", + "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", + "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", + "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", + "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", + "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", + "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", + "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", + "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", + "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", + "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", + "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", + "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", + "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", + "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", + "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", + "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", + "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", + "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", + "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", + "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", + "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", + "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", + "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", + "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", + "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", + "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", + "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", + "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", + "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", + "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", + "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", + "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", + "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", + "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", + "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", + "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", + "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", + "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" ], "index": "pypi", - "version": "==2021.4.4" + "version": "==2021.7.6" }, "requests": { "hashes": [ @@ -1094,19 +1085,19 @@ }, "sphinx": { "hashes": [ - "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c", - "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4" + "sha256:4219f14258ca5612a0c85ed9b7222d54da69724d7e9dd92d1819ad1bf65e1ad2", + "sha256:51028bb0d3340eb80bcc1a2d614e8308ac78d226e6b796943daf57920abc1aea" ], "index": "pypi", - "version": "==4.0.2" + "version": "==4.1.0" }, "sphinx-copybutton": { "hashes": [ - "sha256:19850e9c1ed09c899f136ce3cfa304cb3b6d2f508528c19d8f512ccc8c66b0d4", - "sha256:3695987d5e98e3b223471aaed8aa7491e03e9bfc48ed655a91446fd5e30b6c25" + "sha256:4340d33c169dac6dd82dce2c83333412aa786a42dd01a81a8decac3b130dc8b0", + "sha256:8daed13a87afd5013c3a9af3575cc4d5bec052075ccd3db243f895c07a689386" ], "index": "pypi", - "version": "==0.3.3" + "version": "==0.4.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1169,16 +1160,24 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "index": "pypi", + "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:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be", + "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268" + ], + "index": "pypi", + "version": "==1.0.4" + }, "tqdm": { "hashes": [ - "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd", - "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2" + "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64", + "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.61.1" + "version": "==4.61.2" }, "twine": { "hashes": [ @@ -1225,11 +1224,11 @@ }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], "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.5" + "version": "==1.26.6" }, "virtualenv": { "hashes": [ @@ -1299,11 +1298,11 @@ }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" ], "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "version": "==3.5.0" } } } diff --git a/setup.py b/setup.py index 95252eab054..da25deab512 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def get_long_description() -> str: install_requires=[ "click>=7.1.2", "appdirs", - "toml>=0.10.1", + "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", "pathspec>=0.8.1, <1", diff --git a/src/black/files.py b/src/black/files.py index b9cefd317e0..427ad668f48 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -18,7 +18,7 @@ ) from pathspec import PathSpec -import toml +import tomli from black.output import err from black.report import Report @@ -89,9 +89,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 toml.TomlDecodeError + If parsing fails, will raise a tomli.TOMLDecodeError """ - pyproject_toml = toml.load(path_config) + with open(path_config, encoding="utf8") 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()} diff --git a/tests/test.toml b/tests/test.toml index 570c2f26b15..d3ab1e61202 100644 --- a/tests/test.toml +++ b/tests/test.toml @@ -7,3 +7,10 @@ line-length = 79 target-version = ["py36", "py37", "py38"] exclude='\.pyi?$' include='\.py?$' + +[v1.0.0-syntax] +# This shouldn't break Black. +contributors = [ + "Foo Bar ", + { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" } +] From 91773b89097927a2393bc5223295d37ed26e1632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Hild=C3=A9n?= Date: Tue, 13 Jul 2021 20:24:55 +0300 Subject: [PATCH 309/680] Improve AST safety parsing error message (#2304) Co-authored-by: Hasan Ramezani --- CHANGES.md | 1 + src/black/parsing.py | 52 +++++++++++++++++++++++++------------------- tests/test_black.py | 32 --------------------------- 3 files changed, 31 insertions(+), 54 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 611094f71e9..b3224e1c5b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ - Fixed option usage when using the `--code` flag (#2259) - Do not call `uvloop.install()` when _Black_ is used as a library (#2303) - Added `--required-version` option to require a specific version to be running (#2300) +- Provide a more useful error when parsing fails during AST safety checks (#2304) - Fix incorrect custom breakpoint indices when string group contains fake f-strings (#2311) - Fix regression where `R` prefixes would be lowercased for docstrings (#2285) diff --git a/src/black/parsing.py b/src/black/parsing.py index 8e9feea9120..0b8d984cedd 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -3,7 +3,7 @@ """ import ast import sys -from typing import Iterable, Iterator, List, Set, Union +from typing import Iterable, Iterator, List, Set, Union, Tuple # lib2to3 fork from blib2to3.pytree import Node, Leaf @@ -106,28 +106,36 @@ def lib2to3_unparse(node: Node) -> str: return code -def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: +def parse_single_version( + src: str, version: Tuple[int, int] +) -> Union[ast.AST, ast3.AST, ast27.AST]: filename = "" - if sys.version_info >= (3, 8): - # TODO: support Python 4+ ;) - for minor_version in range(sys.version_info[1], 4, -1): - try: - return ast.parse(src, filename, feature_version=(3, minor_version)) - except SyntaxError: - continue - else: - for feature_version in (7, 6): - try: - return ast3.parse(src, filename, feature_version=feature_version) - except SyntaxError: - continue - if ast27.__name__ == "ast": - raise SyntaxError( - "The requested source code has invalid Python 3 syntax.\n" - "If you are trying to format Python 2 files please reinstall Black" - " with the 'python2' extra: `python3 -m pip install black[python2]`." - ) - return ast27.parse(src) + # typed_ast is needed because of feature version limitations in the builtin ast + 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]) + 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]: + # 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: + return parse_single_version(src, version) + except SyntaxError as e: + if not first_error: + first_error = str(e) + + raise SyntaxError(first_error) def stringify_ast( diff --git a/tests/test_black.py b/tests/test_black.py index 42ac119324c..e3be9c71fd4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -451,38 +451,6 @@ def test_skip_magic_trailing_comma(self) -> None: ) self.assertEqual(expected, actual, msg) - @pytest.mark.no_python2 - def test_python2_should_fail_without_optional_install(self) -> None: - if sys.version_info < (3, 8): - self.skipTest( - "Python 3.6 and 3.7 will install typed-ast to work and as such will be" - " able to parse Python 2 syntax without explicitly specifying the" - " python2 extra" - ) - - source = "x = 1234l" - tmp_file = Path(black.dump_to_file(source)) - try: - runner = BlackRunner() - result = runner.invoke(black.main, [str(tmp_file)]) - self.assertEqual(result.exit_code, 123) - finally: - os.unlink(tmp_file) - assert result.stderr_bytes is not None - actual = ( - result.stderr_bytes.decode() - .replace("\n", "") - .replace("\\n", "") - .replace("\\r", "") - .replace("\r", "") - ) - msg = ( - "The requested source code has invalid Python 3 syntax." - "If you are trying to format Python 2 files please reinstall Black" - " with the 'python2' extra: `python3 -m pip install black[python2]`." - ) - self.assertIn(msg, actual) - @pytest.mark.python2 @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: From 756177a617b5d84e7a77c5b085d19980a5a9f1f9 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 15 Jul 2021 20:21:53 -0400 Subject: [PATCH 310/680] Don't include profiling/ to cut down sdist by ~2x (#2362) They seem to be used as test cases for a specific region of formatting that was slow. Now performance testing is probably something end users won't be needing to do, so this is an easy way of reducing the sdist size sigificantly. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000000..5e53af336e2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +prune profiling From 4622e4cb821efdc22d855eac586cb58d55426943 Mon Sep 17 00:00:00 2001 From: pszlazak Date: Fri, 16 Jul 2021 16:26:29 +0200 Subject: [PATCH 311/680] Create Docker tag 'latest_release' (#2374) Docker images created during release process will have extra tag 'latest_release'. This closes #2373. --- .github/workflows/docker.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 419ec74ba3c..4bb41a17906 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,5 +40,14 @@ jobs: 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 == 'created' }} + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: pyfound/black:latest_release + - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} From e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Fri, 16 Jul 2021 07:42:47 -0700 Subject: [PATCH 312/680] Update CHANGES.md for 21.7b0 release (#2376) * Update CHANGES.md for 21.7b0 release * move some changes to the right section * another one Co-authored-by: Jelle Zijlstra --- CHANGES.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b3224e1c5b3..30f809d5057 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.7b0 ### _Black_ @@ -9,6 +9,21 @@ - Add primer support and test for code piped into black via STDIN (#2315) - Fix internal error when `FORCE_OPTIONAL_PARENTHESES` feature is enabled (#2332) - Accept empty stdin (#2346) +- Provide a more useful error when parsing fails during AST safety checks (#2304) + +### Docker + +- Add new `latest_release` tag automation to follow latest black release on docker + images (#2374) + +### Integrations + +- The vim plugin now searches upwards from the directory containing the current buffer + instead of the current working directory for pyproject.toml. (#1871) +- The vim plugin now reads the correct string normalization option in pyproject.toml + (#1869) +- The vim plugin no longer crashes Black when there's boolean values in pyproject.toml + (#1869) ## 21.6b0 @@ -20,7 +35,6 @@ - Fixed option usage when using the `--code` flag (#2259) - Do not call `uvloop.install()` when _Black_ is used as a library (#2303) - Added `--required-version` option to require a specific version to be running (#2300) -- Provide a more useful error when parsing fails during AST safety checks (#2304) - Fix incorrect custom breakpoint indices when string group contains fake f-strings (#2311) - Fix regression where `R` prefixes would be lowercased for docstrings (#2285) @@ -29,15 +43,8 @@ ### Integrations -- The vim plugin now searches upwards from the directory containing the current buffer - instead of the current working directory for pyproject.toml. (#1871) - -### Integrations - -- The vim plugin now reads the correct string normalization option in pyproject.toml - (#1869) -- The vim plugin no longer crashes Black when there's boolean values in pyproject.toml - (#1869) +- The official Black action now supports choosing what version to use, and supports the + major 3 OSes. (#1940) ## 21.5b2 @@ -58,11 +65,6 @@ - Add a lower bound for the `aiohttp-cors` dependency. Only 0.4.0 or higher is supported. (#2231) -### Integrations - -- The official Black action now supports choosing what version to use, and supports the - major 3 OSes. (#1940) - ### Packaging - Release self-contained x86_64 MacOS binaries as part of the GitHub release pipeline From 4dd100bff283b3c60ce67491dd53f47b45f68ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 16 Jul 2021 18:45:47 +0100 Subject: [PATCH 313/680] Use platformdirs over appdirs (#2375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor Signed-off-by: Bernát Gábor --- CHANGES.md | 4 +++ Pipfile | 2 +- Pipfile.lock | 83 +++++++++++++++++++++++++++++----------------- setup.py | 2 +- src/black/cache.py | 2 +- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 30f809d5057..6714a9d9eb2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +- Moved from `appdirs` dependency to `platformdirs` (#2375) + ## 21.7b0 ### _Black_ diff --git a/Pipfile b/Pipfile index e0d5d1a7969..8d75f30885d 100644 --- a/Pipfile +++ b/Pipfile @@ -27,7 +27,7 @@ black = {editable = true, extras = ["d"], path = "."} [packages] aiohttp = ">=3.6.0" aiohttp-cors = ">=0.4.0" -appdirs = "*" +platformdirs= ">=2" click = ">=8.0.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.8.1" diff --git a/Pipfile.lock b/Pipfile.lock index 90923038adb..b553de86e6a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e6b7c19c10b5fba5ba20296b68049fab625b4c6d61db97b79d83a6dfa36d9f6d" + "sha256": "22f8394c2efe89a85bc1886c46c11c63d00a8d5e7d445ff1cf387b98984581a5" }, "pipfile-spec": 6, "requires": {}, @@ -65,14 +65,6 @@ "index": "pypi", "version": "==0.7.0" }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "index": "pypi", - "version": "==1.4.4" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -189,6 +181,14 @@ "index": "pypi", "version": "==0.8.1" }, + "platformdirs": { + "hashes": [ + "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41", + "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa" + ], + "index": "pypi", + "version": "==2.0.2" + }, "regex": { "hashes": [ "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", @@ -402,14 +402,6 @@ ], "version": "==0.7.12" }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "index": "pypi", - "version": "==1.4.4" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -434,6 +426,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.1" }, + "backports.entry-points-selectable": { + "hashes": [ + "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a", + "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc" + ], + "markers": "python_version >= '2.7'", + "version": "==1.1.0" + }, "black": { "editable": true, "extras": [ @@ -443,11 +443,11 @@ }, "bleach": { "hashes": [ - "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", - "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" + "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa", + "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.3.0" + "version": "==3.3.1" }, "certifi": { "hashes": [ @@ -467,8 +467,10 @@ "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", + "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", + "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", @@ -480,6 +482,7 @@ "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", + "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", @@ -493,10 +496,12 @@ "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", + "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", + "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" ], "version": "==1.14.6" @@ -517,6 +522,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.0.0" }, + "charset-normalizer": { + "hashes": [ + "sha256:3c502a35807c9df35697b0f44b1d65008f83071ff29c69677c7c22573bc5a45a", + "sha256:951567c2f7433a70ab63f1be67e5ee05d3925d9423306ecb71a3b272757bcc95" + ], + "markers": "python_version >= '3'", + "version": "==2.0.2" + }, "click": { "hashes": [ "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", @@ -885,6 +898,14 @@ ], "version": "==1.7.1" }, + "platformdirs": { + "hashes": [ + "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41", + "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa" + ], + "index": "pypi", + "version": "==2.0.2" + }, "pre-commit": { "hashes": [ "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378", @@ -1032,11 +1053,11 @@ }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.25.1" + "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": [ @@ -1085,11 +1106,11 @@ }, "sphinx": { "hashes": [ - "sha256:4219f14258ca5612a0c85ed9b7222d54da69724d7e9dd92d1819ad1bf65e1ad2", - "sha256:51028bb0d3340eb80bcc1a2d614e8308ac78d226e6b796943daf57920abc1aea" + "sha256:23c846a1841af998cb736218539bb86d16f5eb95f5760b1966abcd2d584e62b8", + "sha256:3d513088236eef51e5b0adb78b0492eb22cc3b8ccdb0b36dd021173b365d4454" ], "index": "pypi", - "version": "==4.1.0" + "version": "==4.1.1" }, "sphinx-copybutton": { "hashes": [ @@ -1232,11 +1253,11 @@ }, "virtualenv": { "hashes": [ - "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", - "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" + "sha256:51df5d8a2fad5d1b13e088ff38a433475768ff61f202356bb9812c454c20ae45", + "sha256:e4fc84337dce37ba34ef520bf2d4392b392999dbe47df992870dc23230f6b758" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4.7" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==20.6.0" }, "webencodings": { "hashes": [ diff --git a/setup.py b/setup.py index da25deab512..0a00638108c 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_long_description() -> str: zip_safe=False, install_requires=[ "click>=7.1.2", - "appdirs", + "platformdirs>=2", "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", diff --git a/src/black/cache.py b/src/black/cache.py index 017a2a91362..3f165de2ed6 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -6,7 +6,7 @@ import tempfile from typing import Dict, Iterable, Set, Tuple -from appdirs import user_cache_dir +from platformdirs import user_cache_dir from black.mode import Mode From 65abd1006bca13aa5fc12b173bf206dc261bd592 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 16 Jul 2021 19:21:34 -0700 Subject: [PATCH 314/680] add context manager to temporarily change the cwd (#2377) Commit history before merge: * add context manager to temporarily change the cwd * Iterator, not Iterable --- tests/test_black.py | 43 ++++++++++++++++--------------------------- tests/util.py | 14 +++++++++++++- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/test_black.py b/tests/test_black.py index e3be9c71fd4..998ecfcbdeb 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -44,6 +44,7 @@ # Import other test classes from tests.util import ( THIS_DIR, + change_directory, read_data, DETERMINISTIC_HEADER, BlackBaseTestCase, @@ -2009,17 +2010,12 @@ def test_bpo_33660_workaround(self) -> None: return # https://bugs.python.org/issue33660 - - old_cwd = Path.cwd() - try: - root = Path("/") - os.chdir(str(root)) + root = Path("/") + with change_directory(root): path = Path("workspace") / "project" report = black.Report(verbose=True) normalized_path = black.normalize_path_maybe_ignore(path, root, report) self.assertEqual(normalized_path, "workspace/project") - finally: - os.chdir(str(old_cwd)) def test_newline_comment_interaction(self) -> None: source = "class A:\\\r\n# type: ignore\n pass\n" @@ -2170,10 +2166,6 @@ def test_code_option_config(self) -> None: Test that the code option finds the pyproject.toml in the current directory. """ with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: - # Make sure we are in the project root with the pyproject file - if not Path("tests").exists(): - os.chdir("..") - args = ["--code", "print"] CliRunner().invoke(black.main, args) @@ -2192,22 +2184,19 @@ 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: - # Make sure we are in the tests directory - if Path("tests").exists(): - os.chdir("tests") - - args = ["--code", "print"] - CliRunner().invoke(black.main, args) - - pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve() - assert ( - len(parse.mock_calls) >= 1 - ), "Expected config parse to be called with the current directory." - - _, call_args, _ = parse.mock_calls[0] - assert ( - call_args[0].lower() == str(pyproject_path).lower() - ), "Incorrect config loaded." + with change_directory(Path("tests")): + args = ["--code", "print"] + CliRunner().invoke(black.main, args) + + pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve() + assert ( + len(parse.mock_calls) >= 1 + ), "Expected config parse to be called with the current directory." + + _, call_args, _ = parse.mock_calls[0] + assert ( + call_args[0].lower() == str(pyproject_path).lower() + ), "Incorrect config loaded." with open(black.__file__, "r", encoding="utf-8") as _bf: diff --git a/tests/util.py b/tests/util.py index 1e86a3f3d78..e83017f5ad3 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,7 +1,8 @@ import os import unittest from pathlib import Path -from typing import List, Tuple, Any +from typing import Iterator, List, Tuple, Any +from contextlib import contextmanager from functools import partial import black @@ -72,3 +73,14 @@ def read_data_from_file(file_name: Path) -> Tuple[str, str]: # 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" + + +@contextmanager +def change_directory(path: Path) -> Iterator[None]: + """Context manager to temporarily chdir to a different directory.""" + previous_dir = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(previous_dir) From 6559bdbd9dcf71ba0017d66bb133c744bd83b0e3 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Thu, 22 Jul 2021 15:04:53 +0100 Subject: [PATCH 315/680] isort docs have changed urls (#2390) --- 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 2b3855f7ef0..09421819ec3 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -24,10 +24,10 @@ to conflicting changes. #### Profile Since version 5.0.0, isort supports -[profiles](https://pycqa.github.io/isort/docs/configuration/profiles/) to allow easy +[profiles](https://pycqa.github.io/isort/docs/configuration/profiles.html) to allow easy interoperability with common code styles. You can set the black profile in any of the -[config files](https://pycqa.github.io/isort/docs/configuration/config_files/) supported -by isort. Below, an example for `pyproject.toml`: +[config files](https://pycqa.github.io/isort/docs/configuration/config_files.html) +supported by isort. Below, an example for `pyproject.toml`: ```toml [tool.isort] From 6665677495974cb56e12d717d4e796bbc211dc6a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 24 Jul 2021 15:59:53 -0700 Subject: [PATCH 316/680] Clarify contributing docs (#2398) "as configurable as gofmt" means little to people who haven't used gofmt. --- docs/contributing/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index 7e3a90905cd..480dbd6cd0e 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -15,8 +15,9 @@ Welcome! Happy to see you willing to make the project better. Have you read the .. rubric:: Bird's eye view -In terms of inspiration, *Black* is about as configurable as *gofmt*. This is -deliberate. +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. From 982e7fd9deae3f755a4d5bc1a7757057a98d6245 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 24 Jul 2021 20:04:01 -0400 Subject: [PATCH 317/680] Add ESP to sqlalchemy for black-primer (#2400) The crash has been fixed for a little while now. Tentatively assuming that this will lead to changes. --- src/black_primer/primer.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 90643987942..9665b491a81 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -112,9 +112,8 @@ "py_versions": ["all"] }, "sqlalchemy": { - "no_cli_args_reason": "breaks black with new string parsing - #2188", - "cli_arguments": [], - "expect_formatting_changes": false, + "cli_arguments": ["--experimental-string-processing"], + "expect_formatting_changes": true, "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git", "long_checkout": false, "py_versions": ["all"] From 4760b6e71e73809f8197a111486fb692cbc19692 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Tue, 27 Jul 2021 20:38:04 -0400 Subject: [PATCH 318/680] Fix issue templates + add docs template (#2399) The template weren't applying the default labels ever since I renamed the labels. There has been enough issues about documentation opened recently so it's probably worth a template for it. --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- .github/ISSUE_TEMPLATE/config.yml | 12 ++++++++++ .github/ISSUE_TEMPLATE/docs-issue.md | 27 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/style_issue.md | 2 +- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/docs-issue.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9924408b823..e8d232c8b34 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,8 +1,8 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a report to help us improve Black's quality title: "" -labels: bug +labels: "T: bug" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..67c3ce825ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +# See also: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser + +# This is the default and blank issues are useful so let's keep 'em. +blank_issues_enabled: true + +contact_links: + - name: Chat on Python Discord + url: https://discord.gg/RtVdv86PrH + about: | + User support, questions, and other lightweight requests can be + handled via the \#black-formatter text channel we have on Python + Discord. diff --git a/.github/ISSUE_TEMPLATE/docs-issue.md b/.github/ISSUE_TEMPLATE/docs-issue.md new file mode 100644 index 00000000000..d362b867eab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs-issue.md @@ -0,0 +1,27 @@ +--- +name: Documentation +about: Report a problem with or suggest something for the documentation +title: "" +labels: "T: documentation" +assignees: "" +--- + +**Is this related to a problem? Please describe.** + + + +**Describe the solution you'd like** + + + +**Describe alternatives you've considered** + + + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7c8cd1c1a07..a34e4a0e214 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: "" -labels: enhancement +labels: "T: enhancement" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/style_issue.md b/.github/ISSUE_TEMPLATE/style_issue.md index 2ffd102712d..2e4343a3527 100644 --- a/.github/ISSUE_TEMPLATE/style_issue.md +++ b/.github/ISSUE_TEMPLATE/style_issue.md @@ -2,7 +2,7 @@ name: Style issue about: Help us improve the Black style title: "" -labels: design +labels: "T: design" assignees: "" --- From 8ea641eed5b9540287a8e9a9afa1458b72b9b630 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 28 Jul 2021 17:29:11 +0300 Subject: [PATCH 319/680] Test on Python 3.10-dev (#2406) --- .github/workflows/fuzz.yml | 2 +- .github/workflows/primer.yml | 2 +- .github/workflows/test.yml | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index f3b688f3ef6..6b3ca6bb068 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] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index 5f41c301737..8f7c11c824e 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] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] os: [ubuntu-latest, windows-latest] steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1db9f5d6d93..8e6b4a7a72d 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] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/tox.ini b/tox.ini index a97d18946b3..3767f98a73b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39},fuzz +envlist = {,ci-}py{36,37,38,39,310},fuzz [testenv] setenv = PYTHONPATH = {toxinidir}/src From e76adbecb8c3b62631868332c3b632363c7c16b4 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:53:24 +0300 Subject: [PATCH 320/680] Fix type dependencies of mypy invocation (#2411) Commit history before merge: * Fix type dependencies of mypy invocation * Consistent version upper bound --- .pre-commit-config.yaml | 3 +- Pipfile | 1 - Pipfile.lock | 254 ++++++++++++++++++---------------------- src/black/files.py | 2 +- 4 files changed, 118 insertions(+), 142 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79795d1085f..ff9589bb3ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,9 +25,10 @@ repos: exclude: ^docs/conf.py additional_dependencies: - types-dataclasses >= 0.1.3 - - types-toml >= 0.1.1 + - tomli >= 0.2.6, < 2.0.0 - types-typed-ast >= 1.4.1 - click >= 8.0.0 + - platformdirs >= 2.1.0 - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.3.1 diff --git a/Pipfile b/Pipfile index 8d75f30885d..6527958c425 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,6 @@ flake8 = "*" flake8-bugbear = "*" mypy = ">=0.812" types-dataclasses = ">=0.1.3" -types-toml = ">=0.1.1" types-typed-ast = ">=1.4.1" pre-commit = "*" readme_renderer = "*" diff --git a/Pipfile.lock b/Pipfile.lock index b553de86e6a..3047a487657 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "22f8394c2efe89a85bc1886c46c11c63d00a8d5e7d445ff1cf387b98984581a5" + "sha256": "a02de7783dc48a2466cf52fc639b76f39823ccc87cee228521905f5fdb830b1f" }, "pipfile-spec": 6, "requires": {}, @@ -175,66 +175,58 @@ }, "pathspec": { "hashes": [ - "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", - "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" ], "index": "pypi", - "version": "==0.8.1" + "version": "==0.9.0" }, "platformdirs": { "hashes": [ - "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41", - "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa" + "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", + "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.2.0" }, "regex": { "hashes": [ - "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", - "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", - "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", - "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", - "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", - "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", - "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", - "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", - "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", - "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", - "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", - "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", - "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", - "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", - "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", - "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", - "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", - "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", - "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", - "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", - "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", - "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", - "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", - "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", - "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", - "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", - "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", - "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", - "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", - "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", - "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", - "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", - "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", - "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", - "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", - "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", - "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", - "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", - "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", - "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", - "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" + "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b", + "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16", + "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da", + "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d", + "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba", + "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1", + "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c", + "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281", + "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576", + "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83", + "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39", + "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3", + "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee", + "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce", + "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20", + "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9", + "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a", + "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6", + "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d", + "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d", + "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b", + "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d", + "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16", + "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363", + "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f", + "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a", + "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91", + "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80", + "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531", + "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b", + "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6", + "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c", + "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6" ], "index": "pypi", - "version": "==2021.7.6" + "version": "==2021.8.3" }, "setuptools-scm": { "hashes": [ @@ -246,11 +238,11 @@ }, "tomli": { "hashes": [ - "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be", - "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268" + "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", + "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" ], "index": "pypi", - "version": "==1.0.4" + "version": "==1.2.1" }, "typed-ast": { "hashes": [ @@ -443,11 +435,11 @@ }, "bleach": { "hashes": [ - "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa", - "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78" + "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d", + "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.3.1" + "markers": "python_version >= '3.6'", + "version": "==4.0.0" }, "certifi": { "hashes": [ @@ -524,11 +516,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:3c502a35807c9df35697b0f44b1d65008f83071ff29c69677c7c22573bc5a45a", - "sha256:951567c2f7433a70ab63f1be67e5ee05d3925d9423306ecb71a3b272757bcc95" + "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", + "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" ], "markers": "python_version >= '3'", - "version": "==2.0.2" + "version": "==2.0.4" }, "click": { "hashes": [ @@ -663,11 +655,11 @@ }, "identify": { "hashes": [ - "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839", - "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a" + "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d", + "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.11" + "version": "==2.2.12" }, "idna": { "hashes": [ @@ -687,19 +679,19 @@ }, "importlib-metadata": { "hashes": [ - "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", - "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" + "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9", + "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b" ], "markers": "python_version >= '3.6'", - "version": "==4.6.1" + "version": "==4.6.3" }, "jeepney": { "hashes": [ - "sha256:1237cd64c8f7ac3aa4b3f332c4d0fb4a8216f39eaa662ec904302d4d77de5a54", - "sha256:71335e7a4e93817982f473f3507bffc2eff7a544119ab9b73e089c8ba1409ba3" + "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", + "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" ], "markers": "sys_platform == 'linux'", - "version": "==0.7.0" + "version": "==0.7.1" }, "jinja2": { "hashes": [ @@ -885,11 +877,11 @@ }, "pathspec": { "hashes": [ - "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", - "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" ], "index": "pypi", - "version": "==0.8.1" + "version": "==0.9.0" }, "pkginfo": { "hashes": [ @@ -900,11 +892,11 @@ }, "platformdirs": { "hashes": [ - "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41", - "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa" + "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", + "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.2.0" }, "pre-commit": { "hashes": [ @@ -1006,50 +998,42 @@ }, "regex": { "hashes": [ - "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", - "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", - "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", - "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", - "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", - "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", - "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", - "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", - "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", - "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", - "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", - "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", - "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", - "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", - "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", - "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", - "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", - "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", - "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", - "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", - "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", - "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", - "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", - "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", - "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", - "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", - "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", - "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", - "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", - "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", - "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", - "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", - "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", - "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", - "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", - "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", - "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", - "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", - "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", - "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", - "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" + "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b", + "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16", + "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da", + "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d", + "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba", + "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1", + "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c", + "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281", + "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576", + "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83", + "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39", + "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3", + "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee", + "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce", + "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20", + "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9", + "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a", + "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6", + "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d", + "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d", + "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b", + "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d", + "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16", + "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363", + "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f", + "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a", + "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91", + "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80", + "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531", + "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b", + "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6", + "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c", + "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6" ], "index": "pypi", - "version": "==2021.7.6" + "version": "==2021.8.3" }, "requests": { "hashes": [ @@ -1106,11 +1090,11 @@ }, "sphinx": { "hashes": [ - "sha256:23c846a1841af998cb736218539bb86d16f5eb95f5760b1966abcd2d584e62b8", - "sha256:3d513088236eef51e5b0adb78b0492eb22cc3b8ccdb0b36dd021173b365d4454" + "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", + "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" ], "index": "pypi", - "version": "==4.1.1" + "version": "==4.1.2" }, "sphinx-copybutton": { "hashes": [ @@ -1186,27 +1170,27 @@ }, "tomli": { "hashes": [ - "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be", - "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268" + "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", + "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" ], "index": "pypi", - "version": "==1.0.4" + "version": "==1.2.1" }, "tqdm": { "hashes": [ - "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64", - "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a" + "sha256:3642d483b558eec80d3c831e23953582c34d7e4540db86d9e5ed9dad238dabc6", + "sha256:706dea48ee05ba16e936ee91cb3791cd2ea6da348a0e50b46863ff4363ff4340" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.61.2" + "version": "==4.62.0" }, "twine": { "hashes": [ - "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83", - "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b" + "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", + "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936" ], "index": "pypi", - "version": "==3.4.1" + "version": "==3.4.2" }, "types-dataclasses": { "hashes": [ @@ -1216,14 +1200,6 @@ "index": "pypi", "version": "==0.1.5" }, - "types-toml": { - "hashes": [ - "sha256:33ebe67bebaec55a123ecbaa2bd98fe588335d8d8dda2c7ac53502ef5a81a79a", - "sha256:d4add39a90993173d49ff0b069edd122c66ad4cf5c01082b590e380ca670ee1a" - ], - "index": "pypi", - "version": "==0.1.3" - }, "types-typed-ast": { "hashes": [ "sha256:014ab2742015d3eaab86ea061c27809c7e76beeb9e134102f2e2ddb34ab05c62", @@ -1253,11 +1229,11 @@ }, "virtualenv": { "hashes": [ - "sha256:51df5d8a2fad5d1b13e088ff38a433475768ff61f202356bb9812c454c20ae45", - "sha256:e4fc84337dce37ba34ef520bf2d4392b392999dbe47df992870dc23230f6b758" + "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094", + "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.6.0" + "version": "==20.7.0" }, "webencodings": { "hashes": [ diff --git a/src/black/files.py b/src/black/files.py index 427ad668f48..213bbf82a58 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -92,7 +92,7 @@ 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.load(f) + pyproject_toml = tomli.load(f) # type: ignore # due to deprecated API usage config = pyproject_toml.get("tool", {}).get("black", {}) return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} From b1d060101626aa1c332f52e4bdf0ae5e4cc07990 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Fri, 6 Aug 2021 21:57:46 +0100 Subject: [PATCH 321/680] Jupyter notebook support (#2357) To summarise, based on what was discussed in that issue: due to not being able to parse automagics (e.g. pip install black) without a running IPython kernel, cells with syntax which is parseable by neither ast.parse nor IPython will be skipped cells with multiline magics will be skipped trailing semicolons will be preserved, as they are often put there intentionally in Jupyter Notebooks to suppress unnecessary output Commit history before merge (excluding merge commits): * wip * fixup tests * skip tests if no IPython * install test requirements in ipynb tests * if --ipynb format all as ipynb * wip * add some whole-notebook tests * docstrings * skip multiline magics * add test for nested cell magic * remove ipynb_test.yml, put ipynb tests in tox.ini * add changelog entry * typo * make token same length as magic it replaces * only include .ipynb by default if jupyter dependencies are found * remove logic from const * fixup * fixup * re.compile * noop * clear up * new_src -> dst * early exit for non-python notebooks * add non-python test notebook * add repo with many notebooks to black-primer * install extra dependencies for black-primer * fix planetary computer examples url * dont run on ipynb files by default * add scikit-lego (Expected to change) to black-primer * add ipynb-specific diff * fixup * run on all (including ipynb) by default * remove --include .ipynb from scikit-lego black-primer * use tokenize so as to mirror the exact logic in IPython.core.displayhooks quiet * fixup * :art: * clarify docstring * add test for when comment is after trailing semicolon * enumerate(reversed) instead of [::-1] * clarify docstrings * wip * use jupyter and no_jupyter marks * use THIS_DIR * windows fixup * perform safe check cell-by-cell for ipynb * only perform safe check in ipynb if not fast * remove redundant Optional * :art: * use typeguard * dont process cell containing transformed magic * require typing extensions before 3.10 so as to have TypeGuard * use dataclasses * mention black[jupyter] in docs as well as in README * add faq * add message to assertion error * add test for indented quieted cell * use tokenize_rt else we cant roundtrip * fmake fronzet set for tokens to ignore when looking for trailing semicolon * remove planetary code examples as recent commits result in changes * use dataclasses which inherit from ast.NodeVisitor * bump typing-extensions so that TypeGuard is available * bump typing-extensions in Pipfile * add test with notebook with empty metadata * pipenv lock * deprivative validate_cell * Update README.md * Update docs/getting_started.md * dont cache notebooks if jupyter dependencies arent found * dont write to cache if jupyter deps are not installed * add notebook which cant be parsed * use clirunner * remove other subprocess calls * add docstring * make verbose and quiet keyword only * :art: * run second many test on directory, not on file * test for warning message when running on directory * early return from non-python cell magics * move NothingChanged to report to avoid circular import * remove circular import * reinstate --ipynb flag Co-authored-by: Jelle Zijlstra --- .github/workflows/primer.yml | 2 +- .gitignore | 1 + .pre-commit-hooks.yaml | 11 + CHANGES.md | 5 +- Pipfile | 2 +- README.md | 3 +- docs/faq.md | 23 + docs/getting_started.md | 3 +- pyproject.toml | 1 + setup.py | 3 +- src/black/__init__.py | 191 +++++++- src/black/const.py | 2 +- src/black/files.py | 10 + src/black/handle_ipynb_magics.py | 457 ++++++++++++++++++ src/black/mode.py | 2 + src/black/output.py | 18 + src/black/report.py | 4 + src/black_primer/primer.json | 7 + tests/data/non_python_notebook.ipynb | 1 + tests/data/notebook_empty_metadata.ipynb | 27 ++ tests/data/notebook_no_trailing_newline.ipynb | 39 ++ tests/data/notebook_trailing_newline.ipynb | 39 ++ .../data/notebook_which_cant_be_parsed.ipynb | 1 + tests/data/notebook_without_changes.ipynb | 46 ++ tests/test_black.py | 14 + tests/test_ipynb.py | 455 +++++++++++++++++ tests/test_no_ipynb.py | 37 ++ tox.ini | 7 + 28 files changed, 1383 insertions(+), 28 deletions(-) create mode 100644 src/black/handle_ipynb_magics.py create mode 100644 tests/data/non_python_notebook.ipynb create mode 100644 tests/data/notebook_empty_metadata.ipynb create mode 100644 tests/data/notebook_no_trailing_newline.ipynb create mode 100644 tests/data/notebook_trailing_newline.ipynb create mode 100644 tests/data/notebook_which_cant_be_parsed.ipynb create mode 100644 tests/data/notebook_without_changes.ipynb create mode 100644 tests/test_ipynb.py create mode 100644 tests/test_no_ipynb.py diff --git a/.github/workflows/primer.yml b/.github/workflows/primer.yml index 8f7c11c824e..01eb4ef6187 100644 --- a/.github/workflows/primer.yml +++ b/.github/workflows/primer.yml @@ -38,7 +38,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e ".[d]" + python -m pip install -e ".[d,jupyter]" - name: Primer run env: diff --git a/.gitignore b/.gitignore index ab796ce4cd0..f81bce8fd4e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ src/_black_version.py *.swp .hypothesis/ venv/ +.ipynb_checkpoints/ diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index de2eb674e0d..81848d7dcf7 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -7,3 +7,14 @@ minimum_pre_commit_version: 2.9.2 require_serial: true types_or: [python, pyi] +- id: black-jupyter + name: black-jupyter + description: + "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" + entry: black + language: python + language_version: python3 + minimum_pre_commit_version: 2.9.2 + require_serial: true + types_or: [python, pyi, jupyter] + additional_dependencies: [".[jupyter]"] diff --git a/CHANGES.md b/CHANGES.md index 6714a9d9eb2..a678aaefc2a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,10 @@ ## Unreleased -- Moved from `appdirs` dependency to `platformdirs` (#2375) +### _Black_ + +- Add support for formatting Jupyter Notebook files (#2357) +- Move from `appdirs` dependency to `platformdirs` (#2375) ## 21.7b0 diff --git a/Pipfile b/Pipfile index 6527958c425..433a3b392f7 100644 --- a/Pipfile +++ b/Pipfile @@ -33,6 +33,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.8","version >=" = "3.7.4"} +typing_extensions = {"python_version <" = "3.10","version >=" = "3.10.0.0"} black = {editable = true,extras = ["d"],path = "."} dataclasses = {"python_version <" = "3.7","version >" = "0.1.3"} diff --git a/README.md b/README.md index 7b87a03fc38..709478e1d58 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the _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]`. +`pip install black[python2]`. 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/faq.md b/docs/faq.md index ac5ba937c1c..d7e6a16351f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -37,6 +37,29 @@ Most likely because it is ignored in `.gitignore` or excluded with configuration [file collection and discovery](usage_and_configuration/file_collection_and_discovery.md) for details. +## Why is my Jupyter Notebook cell not formatted? + +_Black_ is timid about formatting Jupyter Notebooks. Cells containing any of the +following will not be formatted: + +- automagics (e.g. `pip install black`) +- multiline magics, e.g.: + + ```python + %timeit f(1, \ + 2, \ + 3) + ``` + +- code which `IPython`'s `TransformerManager` would transform magics into, e.g.: + + ```python + get_ipython().system('ls') + ``` + +- invalid syntax, as it can't be safely distinguished from automagics in the absense of + a running `IPython` kernel. + ## Why are Flake8's E203 and W503 violated? Because they go against PEP 8. E203 falsely triggers on list diff --git a/docs/getting_started.md b/docs/getting_started.md index a509d34e903..c79dc607c4a 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -18,7 +18,8 @@ Also, you can try out _Black_ online for minimal fuss on the _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]`. +dependency, which be installed with `pip install black[python2]`. 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/pyproject.toml b/pyproject.toml index 79060fc422d..d085c0ddc62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,4 +31,5 @@ build-backend = "setuptools.build_meta" 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 0a00638108c..92b78f1abe1 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def get_long_description() -> str: "regex>=2020.1.8", "pathspec>=0.8.1, <1", "dataclasses>=0.6; python_version < '3.7'", - "typing_extensions>=3.7.4; python_version < '3.8'", + "typing_extensions>=3.10.0.0; python_version < '3.10'", "mypy_extensions>=0.4.3", ], extras_require={ @@ -87,6 +87,7 @@ def get_long_description() -> str: "colorama": ["colorama>=0.4.3"], "python2": ["typed-ast>=1.4.2"], "uvloop": ["uvloop>=0.15.2"], + "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"], }, test_suite="tests.test_black", classifiers=[ diff --git a/src/black/__init__.py b/src/black/__init__.py index 51384fb08da..29fb244f8b7 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1,4 +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 @@ -18,6 +20,7 @@ Generator, Iterator, List, + MutableMapping, Optional, Pattern, Set, @@ -39,13 +42,21 @@ 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, diff, color_diff, out, err -from black.report import Report, Changed +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 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, + jupyter_dependencies_are_installed, +) # lib2to3 fork @@ -60,10 +71,6 @@ NewLine = str -class NothingChanged(UserWarning): - """Raised when reformatted code is the same as source.""" - - class WriteBack(Enum): NO = 0 YES = 1 @@ -196,6 +203,14 @@ def validate_regex( " 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)." + ), +) @click.option( "-S", "--skip-string-normalization", @@ -355,6 +370,7 @@ def main( color: bool, fast: bool, pyi: bool, + ipynb: bool, skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, @@ -380,6 +396,9 @@ def main( f" the running version `{__version__}`!" ) ctx.exit(1) + if ipynb and pyi: + err("Cannot pass both `pyi` and `ipynb` flags!") + ctx.exit(1) write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) if target_version: @@ -391,6 +410,7 @@ def main( target_versions=versions, line_length=line_length, is_pyi=pyi, + is_ipynb=ipynb, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, @@ -504,6 +524,11 @@ def get_sources( if is_stdin: p = Path(f"{STDIN_PLACEHOLDER}{str(p)}") + if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed( + verbose=verbose, quiet=quiet + ): + continue + sources.add(p) elif p.is_dir(): sources.update( @@ -516,6 +541,8 @@ def get_sources( force_exclude, report, gitignore, + verbose=verbose, + quiet=quiet, ) ) elif s == "-": @@ -585,6 +612,8 @@ def reformat_one( if is_stdin: if src.suffix == ".pyi": 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): changed = Changed.YES else: @@ -733,6 +762,8 @@ def format_file_in_place( """ if src.suffix == ".pyi": mode = replace(mode, is_pyi=True) + elif src.suffix == ".ipynb": + mode = replace(mode, is_ipynb=True) then = datetime.utcfromtimestamp(src.stat().st_mtime) with open(src, "rb") as buf: @@ -741,6 +772,8 @@ def format_file_in_place( dst_contents = format_file_contents(src_contents, fast=fast, mode=mode) except NothingChanged: return False + except JSONDecodeError: + raise ValueError(f"File '{src}' cannot be parsed as valid Jupyter notebook.") if write_back == WriteBack.YES: with open(src, "w", encoding=encoding, newline=newline) as f: @@ -749,7 +782,10 @@ def format_file_in_place( now = datetime.utcnow() src_name = f"{src}\t{then} +0000" dst_name = f"{src}\t{now} +0000" - diff_contents = diff(src_contents, dst_contents, src_name, dst_name) + if mode.is_ipynb: + diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name) + else: + diff_contents = diff(src_contents, dst_contents, src_name, dst_name) if write_back == WriteBack.COLOR_DIFF: diff_contents = color_diff(diff_contents) @@ -819,6 +855,29 @@ def format_stdin_to_stdout( f.detach() +def check_stability_and_equivalence( + src_contents: str, dst_contents: str, *, mode: Mode +) -> None: + """Perform stability and equivalence checks. + + Raise AssertionError if source and destination contents are not + equivalent, or if a second pass of the formatter would format the + 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`. + + def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: """Reformat contents of a file and return new contents. @@ -829,26 +888,116 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo if not src_contents.strip(): raise NothingChanged - dst_contents = format_str(src_contents, mode=mode) + if mode.is_ipynb: + dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode) + else: + dst_contents = format_str(src_contents, mode=mode) if src_contents == dst_contents: raise NothingChanged - if not fast: - 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`. + 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) return dst_contents +def validate_cell(src: str) -> None: + """Check that cell does not already contain TransformerManager transformations. + + If a cell contains ``!ls``, then it'll be transformed to + ``get_ipython().system('ls')``. However, if the cell originally contained + ``get_ipython().system('ls')``, then it would get transformed in the same way: + + >>> TransformerManager().transform_cell("get_ipython().system('ls')") + "get_ipython().system('ls')\n" + >>> TransformerManager().transform_cell("!ls") + "get_ipython().system('ls')\n" + + Due to the impossibility of safely roundtripping in such situations, cells + containing transformed magics will be ignored. + """ + if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS): + raise NothingChanged + + +def format_cell(src: str, *, fast: bool, mode: Mode) -> str: + """Format code in given cell of Jupyter notebook. + + General idea is: + + - if cell has trailing semicolon, remove it; + - if cell has IPython magics, mask them; + - format cell; + - reinstate IPython magics; + - reinstate trailing semicolon (if originally present); + - strip trailing newlines. + + Cells with syntax errors will not be processed, as they + could potentially be automagics or multi-line magics, which + are currently not supported. + """ + validate_cell(src) + src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( + src + ) + try: + masked_src, replacements = mask_cell(src_without_trailing_semicolon) + except SyntaxError: + raise NothingChanged + masked_dst = format_str(masked_src, mode=mode) + if not fast: + check_stability_and_equivalence(masked_src, masked_dst, mode=mode) + dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) + dst = put_trailing_semicolon_back( + dst_without_trailing_semicolon, has_trailing_semicolon + ) + dst = dst.rstrip("\n") + if dst == src: + raise NothingChanged + return dst + + +def validate_metadata(nb: MutableMapping[str, Any]) -> None: + """If notebook is marked as non-Python, don't format it. + + All notebook metadata fields are optional, see + https://nbformat.readthedocs.io/en/latest/format_description.html. So + if a notebook has empty metadata, we will try to parse it anyway. + """ + language = nb.get("metadata", {}).get("language_info", {}).get("name", None) + if language is not None and language != "python": + raise NothingChanged + + +def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: + """Format Jupyter notebook. + + Operate cell-by-cell, only on code cells, only for Python notebooks. + If the ``.ipynb`` originally had a trailing newline, it'll be preseved. + """ + trailing_newline = src_contents[-1] == "\n" + modified = False + nb = json.loads(src_contents) + validate_metadata(nb) + for cell in nb["cells"]: + if cell.get("cell_type", None) == "code": + try: + src = "".join(cell["source"]) + dst = format_cell(src, fast=fast, mode=mode) + except NothingChanged: + pass + else: + cell["source"] = dst.splitlines(keepends=True) + modified = True + if modified: + dst_contents = json.dumps(nb, indent=1, ensure_ascii=False) + if trailing_newline: + dst_contents = dst_contents + "\n" + return dst_contents + else: + raise NothingChanged + + def format_str(src_contents: str, *, mode: Mode) -> FileContent: """Reformat a string and return new contents. diff --git a/src/black/const.py b/src/black/const.py index 821258588ab..dbb4826be0e 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_INCLUDES = r"\.pyi?$" +DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$" STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__" diff --git a/src/black/files.py b/src/black/files.py index 213bbf82a58..ba60c84a275 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -22,6 +22,7 @@ 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 @@ -165,6 +166,9 @@ def gen_python_files( force_exclude: Optional[Pattern[str]], report: Report, gitignore: Optional[PathSpec], + *, + verbose: bool, + quiet: bool, ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, @@ -216,9 +220,15 @@ def gen_python_files( force_exclude, report, gitignore + get_gitignore(child) if gitignore is not None else None, + verbose=verbose, + quiet=quiet, ) elif child.is_file(): + if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed( + verbose=verbose, quiet=quiet + ): + continue include_match = include.search(normalized_path) if include else True if include_match: yield child diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py new file mode 100644 index 00000000000..ad93c444efc --- /dev/null +++ b/src/black/handle_ipynb_magics.py @@ -0,0 +1,457 @@ +"""Functions to process IPython magics with.""" +from functools import lru_cache +import dataclasses +import ast +from typing import Dict + +import secrets +from typing import List, Tuple +import collections + +from typing import Optional +from typing_extensions import TypeGuard +from black.report import NothingChanged +from black.output import out + + +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", + ) +) +NON_PYTHON_CELL_MAGICS = frozenset( + ( + "%%bash", + "%%html", + "%%javascript", + "%%js", + "%%latex", + "%%markdown", + "%%perl", + "%%ruby", + "%%script", + "%%sh", + "%%svg", + "%%writefile", + ) +) + + +@dataclasses.dataclass(frozen=True) +class Replacement: + mask: str + src: str + + +@lru_cache() +def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool: + try: + import IPython # noqa:F401 + import tokenize_rt # noqa:F401 + 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 remove_trailing_semicolon(src: str) -> Tuple[str, bool]: + """Remove trailing semicolon from Jupyter notebook cell. + + For example, + + fig, ax = plt.subplots() + ax.plot(x_data, y_data); # plot data + + would become + + fig, ax = plt.subplots() + ax.plot(x_data, y_data) # plot data + + 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, + ) + + tokens = src_to_tokens(src) + trailing_semicolon = False + for idx, token in reversed_enumerate(tokens): + if token.name in TOKENS_TO_IGNORE: + continue + if token.name == "OP" and token.src == ";": + del tokens[idx] + trailing_semicolon = True + break + if not trailing_semicolon: + return src, False + return tokens_to_src(tokens), True + + +def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: + """Put trailing semicolon back if cell originally had it. + + Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses + ``tokenize_rt`` so that round-tripping works fine. + """ + if not has_trailing_semicolon: + return src + from tokenize_rt import src_to_tokens, tokens_to_src, reversed_enumerate + + tokens = src_to_tokens(src) + for idx, token in reversed_enumerate(tokens): + if token.name in TOKENS_TO_IGNORE: + continue + tokens[idx] = token._replace(src=token.src + ";") + break + else: # pragma: nocover + raise AssertionError( + "INTERNAL ERROR: Was not able to reinstate trailing semicolon. " + "Please report a bug on https://github.com/psf/black/issues. " + ) from None + return str(tokens_to_src(tokens)) + + +def mask_cell(src: str) -> Tuple[str, List[Replacement]]: + """Mask IPython magics so content becomes parseable Python code. + + For example, + + %matplotlib inline + 'foo' + + becomes + + "25716f358c32750e" + 'foo' + + The replacements are returned, along with the transformed code. + """ + replacements: List[Replacement] = [] + try: + ast.parse(src) + except SyntaxError: + # Might have IPython magics, will process below. + pass + else: + # Syntax is fine, nothing to mask, early return. + return src, replacements + + from IPython.core.inputtransformer2 import TransformerManager + + transformer_manager = TransformerManager() + transformed = transformer_manager.transform_cell(src) + transformed, cell_magic_replacements = replace_cell_magics(transformed) + replacements += cell_magic_replacements + transformed = transformer_manager.transform_cell(transformed) + transformed, magic_replacements = replace_magics(transformed) + if len(transformed.splitlines()) != len(src.splitlines()): + # Multi-line magic, not supported. + raise NothingChanged + replacements += magic_replacements + return transformed, replacements + + +def get_token(src: str, magic: str) -> str: + """Return randomly generated token to mask IPython magic with. + + For example, if 'magic' was `%matplotlib inline`, then a possible + token to mask it with would be `"43fdd17f7e5ddc83"`. The token + will be the same length as the magic, and we make sure that it was + not already present anywhere else in the cell. + """ + assert magic + nbytes = max(len(magic) // 2 - 1, 1) + token = secrets.token_hex(nbytes) + counter = 0 + while token in src: # pragma: nocover + token = secrets.token_hex(nbytes) + counter += 1 + if counter > 100: + raise AssertionError( + "INTERNAL ERROR: Black was not able to replace IPython magic. " + "Please report a bug on https://github.com/psf/black/issues. " + f"The magic might be helpful: {magic}" + ) from None + if len(token) + 2 < len(magic): + token = f"{token}." + return f'"{token}"' + + +def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: + """Replace cell magic with token. + + Note that 'src' will already have been processed by IPython's + TransformerManager().transform_cell. + + Example, + + get_ipython().run_cell_magic('t', '-n1', 'ls =!ls\\n') + + becomes + + "a794." + ls =!ls + + The replacement, along with the transformed code, is returned. + """ + replacements: List[Replacement] = [] + + tree = ast.parse(src) + + cell_magic_finder = CellMagicFinder() + 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: + raise NothingChanged + mask = get_token(src, cell_magic_finder.cell_magic.header) + replacements.append(Replacement(mask=mask, src=cell_magic_finder.cell_magic.header)) + return f"{mask}\n{cell_magic_finder.cell_magic.body}", replacements + + +def replace_magics(src: str) -> Tuple[str, List[Replacement]]: + """Replace magics within body of cell. + + Note that 'src' will already have been processed by IPython's + TransformerManager().transform_cell. + + Example, this + + get_ipython().run_line_magic('matplotlib', 'inline') + 'foo' + + becomes + + "5e67db56d490fd39" + 'foo' + + The replacement, along with the transformed code, are returned. + """ + replacements = [] + magic_finder = MagicFinder() + magic_finder.visit(ast.parse(src)) + new_srcs = [] + for i, line in enumerate(src.splitlines(), start=1): + if i in magic_finder.magics: + offsets_and_magics = magic_finder.magics[i] + if len(offsets_and_magics) != 1: # pragma: nocover + raise AssertionError( + f"Expecting one magic per line, got: {offsets_and_magics}\n" + "Please report a bug on https://github.com/psf/black/issues." + ) + col_offset, magic = ( + offsets_and_magics[0].col_offset, + offsets_and_magics[0].magic, + ) + mask = get_token(src, magic) + replacements.append(Replacement(mask=mask, src=magic)) + line = line[:col_offset] + mask + new_srcs.append(line) + return "\n".join(new_srcs), replacements + + +def unmask_cell(src: str, replacements: List[Replacement]) -> str: + """Remove replacements from cell. + + For example + + "9b20" + foo = bar + + becomes + + %%time + foo = bar + """ + for replacement in replacements: + src = src.replace(replacement.mask, replacement.src) + return src + + +def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]: + """Check if attribute is IPython magic. + + Note that the source of the abstract syntax tree + will already have been processed by IPython's + TransformerManager().transform_cell. + """ + return ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "get_ipython" + ) + + +@dataclasses.dataclass(frozen=True) +class CellMagic: + header: str + body: str + + +@dataclasses.dataclass +class CellMagicFinder(ast.NodeVisitor): + """Find cell magics. + + Note that the source of the abstract syntax tree + will already have been processed by IPython's + TransformerManager().transform_cell. + + For example, + + %%time\nfoo() + + would have been transformed to + + get_ipython().run_cell_magic('time', '', 'foo()\\n') + + and we look for instances of the latter. + """ + + cell_magic: Optional[CellMagic] = None + + def visit_Expr(self, node: ast.Expr) -> None: + """Find cell magic, extract header and body.""" + if ( + isinstance(node.value, ast.Call) + 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]) + self.generic_visit(node) + + +@dataclasses.dataclass(frozen=True) +class OffsetAndMagic: + col_offset: int + magic: str + + +@dataclasses.dataclass +class MagicFinder(ast.NodeVisitor): + """Visit cell to look for get_ipython calls. + + Note that the source of the abstract syntax tree + will already have been processed by IPython's + TransformerManager().transform_cell. + + For example, + + %matplotlib inline + + would have been transformed to + + get_ipython().run_line_magic('matplotlib', 'inline') + + and we look for instances of the latter (and likewise for other + types of magics). + """ + + magics: Dict[int, List[OffsetAndMagic]] = dataclasses.field( + default_factory=lambda: collections.defaultdict(list) + ) + + def visit_Assign(self, node: ast.Assign) -> None: + """Look for system assign magics. + + For example, + + black_version = !black --version + + would have been transformed to + + black_version = get_ipython().getoutput('black --version') + + and we look for instances of the latter. + """ + if ( + isinstance(node.value, ast.Call) + 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]}" + self.magics[node.value.lineno].append( + OffsetAndMagic(node.value.col_offset, src) + ) + self.generic_visit(node) + + def visit_Expr(self, node: ast.Expr) -> None: + """Look for magics in body of cell. + + For examples, + + !ls + !!ls + ?ls + ??ls + + would (respectively) get transformed to + + get_ipython().system('ls') + get_ipython().getoutput('ls') + get_ipython().run_line_magic('pinfo', 'ls') + get_ipython().run_line_magic('pinfo2', 'ls') + + 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 + if node.value.func.attr == "run_line_magic": + if args[0] == "pinfo": + src = f"?{args[1]}" + elif args[0] == "pinfo2": + src = f"??{args[1]}" + 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]}" + elif node.value.func.attr == "getoutput": + src = f"!!{args[0]}" + else: + raise NothingChanged # unsupported magic. + self.magics[node.value.lineno].append( + OffsetAndMagic(node.value.col_offset, src) + ) + self.generic_visit(node) diff --git a/src/black/mode.py b/src/black/mode.py index e2ce322da5c..0b7624eaf8a 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -101,6 +101,7 @@ class Mode: line_length: int = DEFAULT_LINE_LENGTH string_normalization: bool = True is_pyi: bool = False + is_ipynb: bool = False magic_trailing_comma: bool = True experimental_string_processing: bool = False @@ -117,6 +118,7 @@ def get_cache_key(self) -> str: str(self.line_length), str(int(self.string_normalization)), str(int(self.is_pyi)), + str(int(self.is_ipynb)), str(int(self.magic_trailing_comma)), str(int(self.experimental_string_processing)), ] diff --git a/src/black/output.py b/src/black/output.py index c253c85e90e..fd3dbb37627 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -3,6 +3,7 @@ The double calls are for patching purposes in tests. """ +import json from typing import Any, Optional from mypy_extensions import mypyc_attr import tempfile @@ -34,6 +35,23 @@ def err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None: _err(message, nl=nl, **styles) +def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str: + """Return a unified diff string between each cell in notebooks `a` and `b`.""" + a_nb = json.loads(a) + b_nb = json.loads(b) + diff_lines = [ + diff( + "".join(a_nb["cells"][cell_number]["source"]) + "\n", + "".join(b_nb["cells"][cell_number]["source"]) + "\n", + f"{a_name}:cell_{cell_number}", + f"{b_name}:cell_{cell_number}", + ) + for cell_number, cell in enumerate(a_nb["cells"]) + if cell["cell_type"] == "code" + ] + return "".join(diff_lines) + + 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 diff --git a/src/black/report.py b/src/black/report.py index 8fc5da2e167..7e1c8b4b87f 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -16,6 +16,10 @@ class Changed(Enum): YES = 2 +class NothingChanged(UserWarning): + """Raised when reformatted code is the same as source.""" + + @dataclass class Report: """Provides a reformatting counter. Can be rendered with `str(report)`.""" diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 9665b491a81..edbed3f33dd 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -111,6 +111,13 @@ "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"] + }, "sqlalchemy": { "cli_arguments": ["--experimental-string-processing"], "expect_formatting_changes": true, diff --git a/tests/data/non_python_notebook.ipynb b/tests/data/non_python_notebook.ipynb new file mode 100644 index 00000000000..da5cdd8e185 --- /dev/null +++ b/tests/data/non_python_notebook.ipynb @@ -0,0 +1 @@ +{"metadata":{"kernelspec":{"name":"ir","display_name":"R","language":"R"},"language_info":{"name":"R","codemirror_mode":"r","pygments_lexer":"r","mimetype":"text/x-r-source","file_extension":".r","version":"4.0.5"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"library(tidyverse) ","metadata":{"_uuid":"051d70d956493feee0c6d64651c6a088724dca2a","_execution_state":"idle"},"execution_count":null,"outputs":[]}]} \ No newline at end of file diff --git a/tests/data/notebook_empty_metadata.ipynb b/tests/data/notebook_empty_metadata.ipynb new file mode 100644 index 00000000000..7dc1f805cd6 --- /dev/null +++ b/tests/data/notebook_empty_metadata.ipynb @@ -0,0 +1,27 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "print('foo')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/data/notebook_no_trailing_newline.ipynb b/tests/data/notebook_no_trailing_newline.ipynb new file mode 100644 index 00000000000..79f95bea2f6 --- /dev/null +++ b/tests/data/notebook_no_trailing_newline.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "print('foo')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8" + }, + "kernelspec": { + "display_name": "Python 3.8.10 64-bit ('black': venv)", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/tests/data/notebook_trailing_newline.ipynb b/tests/data/notebook_trailing_newline.ipynb new file mode 100644 index 00000000000..4f82869312d --- /dev/null +++ b/tests/data/notebook_trailing_newline.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "print('foo')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8" + }, + "kernelspec": { + "display_name": "Python 3.8.10 64-bit ('black': venv)", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/data/notebook_which_cant_be_parsed.ipynb b/tests/data/notebook_which_cant_be_parsed.ipynb new file mode 100644 index 00000000000..257cc5642cb --- /dev/null +++ b/tests/data/notebook_which_cant_be_parsed.ipynb @@ -0,0 +1 @@ +foo diff --git a/tests/data/notebook_without_changes.ipynb b/tests/data/notebook_without_changes.ipynb new file mode 100644 index 00000000000..ac6c7e63efa --- /dev/null +++ b/tests/data/notebook_without_changes.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "print(\"foo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook should not be reformatted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8" + }, + "kernelspec": { + "display_name": "Python 3.8.10 64-bit ('black': venv)", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 998ecfcbdeb..942446ec343 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1379,6 +1379,8 @@ def test_include_exclude(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1690,6 +1692,8 @@ def test_gitignore_exclude(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1717,6 +1721,8 @@ def test_nested_gitignore(self) -> None: None, report, root_gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1751,6 +1757,8 @@ def test_empty_include(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1775,6 +1783,8 @@ def test_extend_exclude(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1847,6 +1857,8 @@ def test_symlink_out_of_root_directory(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) except ValueError as ve: @@ -1868,6 +1880,8 @@ def test_symlink_out_of_root_directory(self) -> None: None, report, gitignore, + verbose=False, + quiet=False, ) ) path.iterdir.assert_called() diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py new file mode 100644 index 00000000000..038155e9270 --- /dev/null +++ b/tests/test_ipynb.py @@ -0,0 +1,455 @@ +import pathlib +from click.testing import CliRunner +from black.handle_ipynb_magics import jupyter_dependencies_are_installed +from black import ( + main, + NothingChanged, + format_cell, + 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 + +pytestmark = pytest.mark.jupyter +pytest.importorskip("IPython", reason="IPython is an optional dependency") +pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency") + +JUPYTER_MODE = Mode(is_ipynb=True) + +runner = CliRunner() + + +def test_noop() -> None: + src = 'foo = "a"' + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +@pytest.mark.parametrize("fast", [True, False]) +def test_trailing_semicolon(fast: bool) -> None: + src = 'foo = "a" ;' + result = format_cell(src, fast=fast, mode=JUPYTER_MODE) + expected = 'foo = "a";' + assert result == expected + + +def test_trailing_semicolon_with_comment() -> None: + src = 'foo = "a" ; # bar' + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = 'foo = "a"; # bar' + assert result == expected + + +def test_trailing_semicolon_with_comment_on_next_line() -> None: + src = "import black;\n\n# this is a comment" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_trailing_semicolon_indented() -> None: + src = "with foo:\n plot_bar();" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_trailing_semicolon_noop() -> None: + src = 'foo = "a";' + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_cell_magic() -> None: + src = "%%time\nfoo =bar" + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = "%%time\nfoo = bar" + assert result == expected + + +def test_cell_magic_noop() -> None: + src = "%%time\n2 + 2" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +@pytest.mark.parametrize( + "src, expected", + ( + pytest.param("ls =!ls", "ls = !ls", id="System assignment"), + pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"), + pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"), + pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"), + pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"), + pytest.param( + "%matplotlib inline\n'foo'", + '%matplotlib inline\n"foo"', + id="Line magic with argument", + ), + pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"), + ), +) +def test_magic(src: str, expected: str) -> None: + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + assert result == expected + + +@pytest.mark.parametrize( + "src", + ( + "%%bash\n2+2", + "%%html --isolated\n2+2", + ), +) +def test_non_python_magics(src: str) -> None: + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_set_input() -> None: + src = "a = b??" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_input_already_contains_transformed_magic() -> None: + src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")' + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_magic_noop() -> None: + src = "ls = !ls" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_cell_magic_with_magic() -> None: + src = "%%t -n1\nls =!ls" + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = "%%t -n1\nls = !ls" + assert result == expected + + +def test_cell_magic_nested() -> None: + src = "%%time\n%%time\n2+2" + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = "%%time\n%%time\n2 + 2" + assert result == expected + + +def test_cell_magic_with_magic_noop() -> None: + src = "%%t -n1\nls = !ls" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_automagic() -> None: + src = "pip install black" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_multiline_magic() -> None: + src = "%time 1 + \\\n2" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_multiline_no_magic() -> None: + src = "1 + \\\n2" + result = format_cell(src, fast=True, mode=JUPYTER_MODE) + expected = "1 + 2" + assert result == expected + + +def test_cell_magic_with_invalid_body() -> None: + src = "%%time\nif True" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_empty_cell() -> None: + src = "" + with pytest.raises(NothingChanged): + format_cell(src, fast=True, mode=JUPYTER_MODE) + + +def test_entire_notebook_empty_metadata() -> None: + with open( + os.path.join("tests", "data", "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) + expected = ( + "{\n" + ' "cells": [\n' + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {\n' + ' "tags": []\n' + " },\n" + ' "outputs": [],\n' + ' "source": [\n' + ' "%%time\\n",\n' + ' "\\n",\n' + ' "print(\\"foo\\")"\n' + " ]\n" + " },\n" + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {},\n' + ' "outputs": [],\n' + ' "source": []\n' + " }\n" + " ],\n" + ' "metadata": {},\n' + ' "nbformat": 4,\n' + ' "nbformat_minor": 4\n' + "}\n" + ) + assert result == expected + + +def test_entire_notebook_trailing_newline() -> None: + with open( + os.path.join("tests", "data", "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) + expected = ( + "{\n" + ' "cells": [\n' + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {\n' + ' "tags": []\n' + " },\n" + ' "outputs": [],\n' + ' "source": [\n' + ' "%%time\\n",\n' + ' "\\n",\n' + ' "print(\\"foo\\")"\n' + " ]\n" + " },\n" + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {},\n' + ' "outputs": [],\n' + ' "source": []\n' + " }\n" + " ],\n" + ' "metadata": {\n' + ' "interpreter": {\n' + ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa:B950 + " },\n" + ' "kernelspec": {\n' + ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n' + ' "name": "python3"\n' + " },\n" + ' "language_info": {\n' + ' "name": "python",\n' + ' "version": ""\n' + " }\n" + " },\n" + ' "nbformat": 4,\n' + ' "nbformat_minor": 4\n' + "}\n" + ) + assert result == expected + + +def test_entire_notebook_no_trailing_newline() -> None: + with open( + os.path.join("tests", "data", "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) + expected = ( + "{\n" + ' "cells": [\n' + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {\n' + ' "tags": []\n' + " },\n" + ' "outputs": [],\n' + ' "source": [\n' + ' "%%time\\n",\n' + ' "\\n",\n' + ' "print(\\"foo\\")"\n' + " ]\n" + " },\n" + " {\n" + ' "cell_type": "code",\n' + ' "execution_count": null,\n' + ' "metadata": {},\n' + ' "outputs": [],\n' + ' "source": []\n' + " }\n" + " ],\n" + ' "metadata": {\n' + ' "interpreter": {\n' + ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa: B950 + " },\n" + ' "kernelspec": {\n' + ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n' + ' "name": "python3"\n' + " },\n" + ' "language_info": {\n' + ' "name": "python",\n' + ' "version": ""\n' + " }\n" + " },\n" + ' "nbformat": 4,\n' + ' "nbformat_minor": 4\n' + "}" + ) + assert result == expected + + +def test_entire_notebook_without_changes() -> None: + with open( + os.path.join("tests", "data", "notebook_without_changes.ipynb"), "rb" + ) as fd: + content_bytes = fd.read() + content = content_bytes.decode() + with pytest.raises(NothingChanged): + format_file_contents(content, fast=True, mode=JUPYTER_MODE) + + +def test_non_python_notebook() -> None: + with open(os.path.join("tests", "data", "non_python_notebook.ipynb"), "rb") as fd: + content_bytes = fd.read() + content = content_bytes.decode() + with pytest.raises(NothingChanged): + format_file_contents(content, fast=True, mode=JUPYTER_MODE) + + +def test_empty_string() -> None: + with pytest.raises(NothingChanged): + format_file_contents("", fast=True, mode=JUPYTER_MODE) + + +def test_unparseable_notebook() -> None: + msg = ( + r"File 'tests[/\\]data[/\\]notebook_which_cant_be_parsed\.ipynb' " + r"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, + ) + + +def test_ipynb_diff_with_change() -> None: + result = runner.invoke( + main, + [ + os.path.join("tests", "data", "notebook_trailing_newline.ipynb"), + "--diff", + ], + ) + expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' + assert expected in result.output + + +def test_ipynb_diff_with_no_change() -> None: + result = runner.invoke( + main, + [ + os.path.join("tests", "data", "notebook_without_changes.ipynb"), + "--diff", + ], + ) + expected = "1 file would be left unchanged." + assert expected in result.output + + +def test_cache_isnt_written_if_no_jupyter_deps_single( + monkeypatch: MonkeyPatch, tmpdir: local +) -> 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") + tmp_nb = tmpdir / "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")]) + 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")]) + assert "reformatted" in result.output + + +def test_cache_isnt_written_if_no_jupyter_deps_dir( + monkeypatch: MonkeyPatch, tmpdir: local +) -> 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") + tmp_nb = tmpdir / "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)]) + 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)]) + assert "reformatted" in result.output + + +def test_ipynb_flag(tmpdir: local) -> None: + nb = os.path.join("tests", "data", "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()) + result = runner.invoke( + main, + [ + str(tmp_nb), + "--diff", + "--ipynb", + ], + ) + expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n' + assert expected in result.output + + +def test_ipynb_and_pyi_flags() -> None: + nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + result = runner.invoke( + main, + [ + nb, + "--pyi", + "--ipynb", + "--diff", + ], + ) + assert isinstance(result.exception, SystemExit) + expected = "Cannot pass both `pyi` and `ipynb` flags!\n" + assert result.output == expected diff --git a/tests/test_no_ipynb.py b/tests/test_no_ipynb.py new file mode 100644 index 00000000000..bcda2d5369f --- /dev/null +++ b/tests/test_no_ipynb.py @@ -0,0 +1,37 @@ +import pytest +import os + +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 + +runner = CliRunner() + + +def test_ipynb_diff_with_no_change_single() -> None: + jupyter_dependencies_are_installed.cache_clear() + path = THIS_DIR / "data/notebook_trailing_newline.ipynb" + 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" + ) + assert expected_output in result.output + + +def test_ipynb_diff_with_no_change_dir(tmpdir: tmpdir) -> None: + jupyter_dependencies_are_installed.cache_clear() + runner = CliRunner() + nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb") + tmp_nb = tmpdir / "notebook.ipynb" + with open(nb) as src, open(tmp_nb, "w") as dst: + dst.write(src.read()) + result = runner.invoke(main, [str(tmpdir)]) + expected_output = ( + "Skipping .ipynb files as Jupyter dependencies are not installed.\n" + "You can fix this by running ``pip install black[jupyter]``\n" + ) + assert expected_output in result.output diff --git a/tox.ini b/tox.ini index 3767f98a73b..57f41acb3d1 100644 --- a/tox.ini +++ b/tox.ini @@ -16,10 +16,17 @@ commands = pip install -e .[d] coverage erase pytest tests --run-optional no_python2 \ + --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 \ !ci: --numprocesses auto \ --cov --cov-append {posargs} coverage report From 355a6b34b3a61b6729e469115e1393d9779963c5 Mon Sep 17 00:00:00 2001 From: aru Date: Wed, 11 Aug 2021 21:50:46 -0400 Subject: [PATCH 322/680] fix: remove unneccessary escape character (#2423) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 67c3ce825ea..3f6641c91a0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -8,5 +8,5 @@ contact_links: url: https://discord.gg/RtVdv86PrH about: | User support, questions, and other lightweight requests can be - handled via the \#black-formatter text channel we have on Python + handled via the #black-formatter text channel we have on Python Discord. From e465acf6f8dc82152b0e9187ed6e934b97c678c8 Mon Sep 17 00:00:00 2001 From: Tom Fryers <61272761+TomFryers@users.noreply.github.com> Date: Thu, 12 Aug 2021 23:45:33 +0100 Subject: [PATCH 323/680] Update language server links (#2425) python-language-server is no longer maintained. --- docs/integrations/editors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index dd09b4df6d1..b1ca84e42fc 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -295,12 +295,12 @@ Use [sublack plugin](https://github.com/jgirardet/sublack). Use [blackcellmagic](https://github.com/csurfer/blackcellmagic). -## Python Language Server +## Python LSP Server If your editor supports the [Language Server Protocol](https://langserver.org/) (Atom, Sublime Text, Visual Studio Code and many more), you can use the -[Python Language Server](https://github.com/palantir/python-language-server) with the -[pyls-black](https://github.com/rupert/pyls-black) plugin. +[Python LSP Server](https://github.com/python-lsp/python-lsp-server) with the +[python-lsp-black](https://github.com/python-lsp/python-lsp-black) plugin. ## Atom/Nuclide From b92ec348439edb8641204a102849bfab51f4dda0 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Mon, 16 Aug 2021 03:54:42 +0100 Subject: [PATCH 324/680] Add jupyter deps to Pipfile.lock (#2419) --- Pipfile | 2 +- Pipfile.lock | 243 ++++++++++++++++++++++++++++----------------------- 2 files changed, 134 insertions(+), 111 deletions(-) diff --git a/Pipfile b/Pipfile index 433a3b392f7..c9238d3f69e 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" wheel = ">=0.31.1" -black = {editable = true, extras = ["d"], path = "."} +black = {editable = true, extras = ["d", "jupyter"], path = "."} [packages] aiohttp = ">=3.6.0" diff --git a/Pipfile.lock b/Pipfile.lock index 3047a487657..0c9a42348c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a02de7783dc48a2466cf52fc639b76f39823ccc87cee228521905f5fdb830b1f" + "sha256": "eca7ef453dc0d16ab3910fc8035a3b57e2c1512dea8d9a95958eeecfe71935c1" }, "pipfile-spec": 6, "requires": {}, @@ -287,9 +287,9 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.8", + "python_version <": "3.10", "version": "==3.10.0.0", - "version >=": "3.7.4" + "version >=": "3.10.0.0" }, "yarl": { "hashes": [ @@ -394,6 +394,14 @@ ], "version": "==0.7.12" }, + "appnope": { + "hashes": [ + "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", + "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.2" + }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -418,6 +426,13 @@ "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:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a", @@ -448,56 +463,6 @@ ], "version": "==2021.5.30" }, - "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" - }, "cfgv": { "hashes": [ "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1", @@ -596,23 +561,13 @@ "index": "pypi", "version": "==5.5" }, - "cryptography": { - "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + "decorator": { + "hashes": [ + "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323", + "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.7" + "markers": "python_version >= '3.5'", + "version": "==5.0.9" }, "distlib": { "hashes": [ @@ -655,11 +610,11 @@ }, "identify": { "hashes": [ - "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d", - "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541" + "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c", + "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.12" + "version": "==2.2.13" }, "idna": { "hashes": [ @@ -685,13 +640,28 @@ "markers": "python_version >= '3.6'", "version": "==4.6.3" }, - "jeepney": { + "ipython": { "hashes": [ - "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", - "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" + "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e", + "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362" ], - "markers": "sys_platform == 'linux'", - "version": "==0.7.1" + "markers": "python_version >= '3.7'", + "version": "==7.26.0" + }, + "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" }, "jinja2": { "hashes": [ @@ -757,6 +727,14 @@ "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", @@ -875,6 +853,14 @@ "markers": "python_version >= '3.6'", "version": "==21.0" }, + "parso": { + "hashes": [ + "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", + "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.2" + }, "pathspec": { "hashes": [ "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", @@ -883,6 +869,21 @@ "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", @@ -900,11 +901,26 @@ }, "pre-commit": { "hashes": [ - "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378", - "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4" + "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c", + "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4" ], "index": "pypi", - "version": "==2.13.0" + "version": "==2.14.0" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", + "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.19" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" }, "pycodestyle": { "hashes": [ @@ -914,14 +930,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", @@ -1057,14 +1065,6 @@ ], "version": "==1.5.0" }, - "secretstorage": { - "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.1" - }, "setuptools-scm": { "hashes": [ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", @@ -1160,6 +1160,14 @@ "markers": "python_version >= '3.5'", "version": "==1.1.5" }, + "tokenize-rt": { + "hashes": [ + "sha256:ab339b5ff829eb5e198590477f9c03c84e762b3e455e74c018956e7e326cbc70", + "sha256:b37251fa28c21e8cce2e42f7769a35fba2dd2ecafb297208f9a9a8add3ca7793" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==4.1.0" + }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -1184,6 +1192,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.62.0" }, + "traitlets": { + "hashes": [ + "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", + "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0.5" + }, "twine": { "hashes": [ "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", @@ -1194,19 +1210,19 @@ }, "types-dataclasses": { "hashes": [ - "sha256:7b5f4099fb21c209f2df3a83c2b64308c29955769d610a457244dc0eebe1cafc", - "sha256:c19491cfb981bff9cafd9c113c291a7a54adccc6298ded8ca3de0d7abe211984" + "sha256:248075d093d8f7c1541ce515594df7ae40233d1340afde11ce7125368c5209b8", + "sha256:fc372bb68b878ac7a68fd04230d923d4a6303a137ecb0b9700b90630bdfcbfc9" ], "index": "pypi", - "version": "==0.1.5" + "version": "==0.1.7" }, "types-typed-ast": { "hashes": [ - "sha256:014ab2742015d3eaab86ea061c27809c7e76beeb9e134102f2e2ddb34ab05c62", - "sha256:6a47165760321835d160da20a6ac5b9065f23d6dc246162508cad1ef1238afc3" + "sha256:b7f561796b4d002c7522b0020f58b18f715bd28a31429d424a78e2e2dbbd6785", + "sha256:ffa0471e0ba19c4ea0cba0436d660871b5f5215854ea9ead3cb5b60f525af75a" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.4.4" }, "typing-extensions": { "hashes": [ @@ -1215,9 +1231,9 @@ "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "python_version <": "3.8", + "python_version <": "3.10", "version": "==3.10.0.0", - "version >=": "3.7.4" + "version >=": "3.10.0.0" }, "urllib3": { "hashes": [ @@ -1229,11 +1245,18 @@ }, "virtualenv": { "hashes": [ - "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094", - "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5" + "sha256:57bcb59c5898818bd555b1e0cfcf668bd6204bc2b53ad0e70a52413bd790f9e4", + "sha256:73863dc3be1efe6ee638e77495c0c195a6384ae7b15c561f3ceb2698ae7267c1" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.7.0" + "version": "==20.7.1" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" }, "webencodings": { "hashes": [ @@ -1244,11 +1267,11 @@ }, "wheel": { "hashes": [ - "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e", - "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e" + "sha256:21014b2bd93c6d0034b6ba5d35e4eb284340e09d63c59aef6fc14b0f346146fd", + "sha256:e2ef7239991699e3355d54f8e968a21bb940a1dbf34a4d226741e64462516fad" ], "index": "pypi", - "version": "==0.36.2" + "version": "==0.37.0" }, "yarl": { "hashes": [ From ef7c45f28132a7704a4549072ce5b272034fa196 Mon Sep 17 00:00:00 2001 From: Aneesh Agrawal Date: Wed, 18 Aug 2021 09:24:14 -0700 Subject: [PATCH 325/680] Remove `language_version` for pre-commit (#2430) * Remove `language_version` for pre-commit At my company, we set the Python version in `default_language_version` in each repo's `.pre-commit-config.yaml`, so that all hooks are running with the same Python version. However, this currently doesn't work for black, as the `language_version` specified here in the upstream `.pre-commit-hooks.yaml` takes precedence. Currently, this requires us to manually set `language_version` specifically for black, duplicating the value from `default_language_version`. The failure mode otherwise is subtle - black works most of the time, but try to add a walrus operator and it suddenly breaks! Given that black's `setup.py` already has `python_requires>=3.6.2`, specifying that `python3` must be used here isn't needed as folks inadvertently using Python 2 will get hook-install-time failures anyways. Remove the `language_version` from these upstream hook configs so that users of black are able to use `default_language_version` and have it apply to all their hooks, black included. Example `.pre-commit-config.yaml` before: ``` default_language_version: python: python3.8 repos: - repo: https://github.com/psf/black rev: 21.7b0 hooks: - id: black language_version: python3.8 ``` After: ``` default_language_version: python: python3.8 repos: - repo: https://github.com/psf/black rev: 21.7b0 hooks: - id: black ``` * Add changelog entry --- .pre-commit-hooks.yaml | 2 -- CHANGES.md | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 81848d7dcf7..137957045a6 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,7 +3,6 @@ description: "Black: The uncompromising Python code formatter" entry: black language: python - language_version: python3 minimum_pre_commit_version: 2.9.2 require_serial: true types_or: [python, pyi] @@ -13,7 +12,6 @@ "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" entry: black language: python - language_version: python3 minimum_pre_commit_version: 2.9.2 require_serial: true types_or: [python, pyi, jupyter] diff --git a/CHANGES.md b/CHANGES.md index a678aaefc2a..a4b8e01fb93 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,11 @@ - Add support for formatting Jupyter Notebook files (#2357) - Move from `appdirs` dependency to `platformdirs` (#2375) +### Integrations + +- The provided pre-commit hooks no longer specify `language_version` to avoid overriding + `default_language_version` (#2430) + ## 21.7b0 ### _Black_ From 104aec555fae0883ef5b53709569bd9c4d420bc5 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Fri, 20 Aug 2021 16:54:53 -0700 Subject: [PATCH 326/680] Present a more user-friendly error if .gitignore is invalid (#2414) Fixes #2359. This commit now makes Black exit with an user-friendly error message if a .gitignore file couldn't be parsed -- a massive improvement over an opaque traceback! --- CHANGES.md | 1 + setup.py | 2 +- src/black/__init__.py | 28 +++++++++++-------- src/black/files.py | 7 ++++- tests/data/invalid_gitignore_tests/.gitignore | 1 + tests/data/invalid_gitignore_tests/a.py | 0 .../invalid_gitignore_tests/pyproject.toml | 1 + .../data/invalid_nested_gitignore_tests/a.py | 0 .../a/.gitignore | 1 + .../invalid_nested_gitignore_tests/a/a.py | 0 .../pyproject.toml | 1 + tests/test_black.py | 24 ++++++++++++++++ 12 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 tests/data/invalid_gitignore_tests/.gitignore create mode 100644 tests/data/invalid_gitignore_tests/a.py create mode 100644 tests/data/invalid_gitignore_tests/pyproject.toml create mode 100644 tests/data/invalid_nested_gitignore_tests/a.py create mode 100644 tests/data/invalid_nested_gitignore_tests/a/.gitignore create mode 100644 tests/data/invalid_nested_gitignore_tests/a/a.py create mode 100644 tests/data/invalid_nested_gitignore_tests/pyproject.toml diff --git a/CHANGES.md b/CHANGES.md index a4b8e01fb93..3a96029bf5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Add support for formatting Jupyter Notebook files (#2357) - Move from `appdirs` dependency to `platformdirs` (#2375) +- Present a more user-friendly error if .gitignore is invalid (#2414) ### Integrations diff --git a/setup.py b/setup.py index 92b78f1abe1..215fa6cff61 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_long_description() -> str: "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", - "pathspec>=0.8.1, <1", + "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.10.0.0; python_version < '3.10'", "mypy_extensions>=0.4.3", diff --git a/src/black/__init__.py b/src/black/__init__.py index 29fb244f8b7..60f4fa34e9d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -9,6 +9,7 @@ from multiprocessing import Manager, freeze_support import os from pathlib import Path +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import regex as re import signal import sys @@ -428,18 +429,21 @@ def main( content=code, fast=fast, write_back=write_back, mode=mode, report=report ) else: - sources = get_sources( - ctx=ctx, - src=src, - quiet=quiet, - verbose=verbose, - include=include, - exclude=exclude, - extend_exclude=extend_exclude, - force_exclude=force_exclude, - report=report, - stdin_filename=stdin_filename, - ) + try: + sources = get_sources( + ctx=ctx, + src=src, + quiet=quiet, + verbose=verbose, + include=include, + exclude=exclude, + extend_exclude=extend_exclude, + force_exclude=force_exclude, + report=report, + stdin_filename=stdin_filename, + ) + except GitWildMatchPatternError: + ctx.exit(1) path_empty( sources, diff --git a/src/black/files.py b/src/black/files.py index ba60c84a275..4d7b47aaa9f 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -18,6 +18,7 @@ ) from pathspec import PathSpec +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import tomli from black.output import err @@ -122,7 +123,11 @@ def get_gitignore(root: Path) -> PathSpec: if gitignore.is_file(): with gitignore.open(encoding="utf-8") as gf: lines = gf.readlines() - return PathSpec.from_lines("gitwildmatch", lines) + try: + return PathSpec.from_lines("gitwildmatch", lines) + except GitWildMatchPatternError as e: + err(f"Could not parse {gitignore}: {e}") + raise def normalize_path_maybe_ignore( diff --git a/tests/data/invalid_gitignore_tests/.gitignore b/tests/data/invalid_gitignore_tests/.gitignore new file mode 100644 index 00000000000..cdf4cb4feba --- /dev/null +++ b/tests/data/invalid_gitignore_tests/.gitignore @@ -0,0 +1 @@ +! diff --git a/tests/data/invalid_gitignore_tests/a.py b/tests/data/invalid_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_gitignore_tests/pyproject.toml b/tests/data/invalid_gitignore_tests/pyproject.toml new file mode 100644 index 00000000000..3908e457a9e --- /dev/null +++ b/tests/data/invalid_gitignore_tests/pyproject.toml @@ -0,0 +1 @@ +# Empty configuration file; used in tests to avoid interference from Black's own config. diff --git a/tests/data/invalid_nested_gitignore_tests/a.py b/tests/data/invalid_nested_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_nested_gitignore_tests/a/.gitignore b/tests/data/invalid_nested_gitignore_tests/a/.gitignore new file mode 100644 index 00000000000..cdf4cb4feba --- /dev/null +++ b/tests/data/invalid_nested_gitignore_tests/a/.gitignore @@ -0,0 +1 @@ +! diff --git a/tests/data/invalid_nested_gitignore_tests/a/a.py b/tests/data/invalid_nested_gitignore_tests/a/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_nested_gitignore_tests/pyproject.toml b/tests/data/invalid_nested_gitignore_tests/pyproject.toml new file mode 100644 index 00000000000..3908e457a9e --- /dev/null +++ b/tests/data/invalid_nested_gitignore_tests/pyproject.toml @@ -0,0 +1 @@ +# Empty configuration file; used in tests to avoid interference from Black's own config. diff --git a/tests/test_black.py b/tests/test_black.py index 942446ec343..5c720507216 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1727,6 +1727,30 @@ def test_nested_gitignore(self) -> None: ) 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() From 8c04847aa22d14f01bb206cfc1b1e1cebd2ae538 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 22 Aug 2021 22:52:19 -0400 Subject: [PATCH 327/680] Improve f-string expression detection regex so ... (#2437) we don't accidentally add backslashes to them when normalizing quotes because that's invalid syntax! The problem this commit fixes is that matches would eat too much blocking important matches to occur. For example, here's one f-string body: {a}{b}{c} I know there's no risk of introducing backslashes here, but the regex already goes sideways with this. Throwing this example at regex101 I get: {a}{b}{c} # The As and Bs are the two matches, and the upper ---- ---- # case letters are the groups with those matches. aAaa bbBb ... we've missed the middle expression (so if any backslashes in a more complex example were introduced there we wouldn't bail out even though we should -- hence the bug). As it stands the regex needs somesort of extra character (or the start/end of the body) around the expressions but that isn't always the case as shown above. The fix implemented here is to turn the "eat a surrounding non-curly bracket character" groups ie. `(?:[^{]|^)` and `(?:[^}]|$)` into negative lookaheads and lookbehinds. This still guarantees the already specified rules but without problematically eating extra characters ^^ --- CHANGES.md | 2 ++ src/black/strings.py | 4 ++-- tests/data/string_quotes.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3a96029bf5c..22ddc423e55 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ - Add support for formatting Jupyter Notebook files (#2357) - Move from `appdirs` dependency to `platformdirs` (#2375) - Present a more user-friendly error if .gitignore is invalid (#2414) +- The failsafe for accidentally added backslashes in f-string expressions has been + hardened to handle more edge cases during quote normalization (#2437) ### Integrations diff --git a/src/black/strings.py b/src/black/strings.py index 80f588f5119..d7b6c240e80 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -190,9 +190,9 @@ def normalize_string_quotes(s: str) -> str: if "f" in prefix.casefold(): matches = re.findall( r""" - (?:[^{]|^)\{ # start of the string or a non-{ followed by a single { + (?:(? Date: Sun, 22 Aug 2021 23:20:06 -0400 Subject: [PATCH 328/680] Add test requirements to Pipfile[.lock] & bump deps (#2436) While this development environment / requirements situation is a mess, let's at least make it consistent. We're effectively supporting two modes of development in this project, 1) tox based dev commands (e.g. `tox -e fuzz`) that are dead simple to use, and 2) manual dev commands (e.g. `pytest -n auto`) that give more control and are usually faster. Right now the Pipfile.lock based development environment is incomplete missing the test requirements specified in ./test_requirements.txt. This is annoying since manual test commands (e.g. `pytest -k fmtonoff`) fail. Let's fix this by making Pipfile.lock basically a "everything you need" requirements file (fuzzing not included since running it locally is not something common). Oh and let's bump some documentation deps (and bring some requirements across .pre-commit-config.yaml, Pipfile, and docs/requirement.txt in alignment again). Don't worry, I tested these changes so they should be fine (hopefully!). --- .pre-commit-config.yaml | 4 +- Pipfile | 33 ++- Pipfile.lock | 551 +++++++++++++++++++++++++++++++--------- docs/requirements.txt | 6 +- 4 files changed, 457 insertions(+), 137 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff9589bb3ce..5ae418d9bdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.902 + rev: v0.910 hooks: - id: mypy exclude: ^docs/conf.py @@ -31,7 +31,7 @@ repos: - platformdirs >= 2.1.0 - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.3.1 + rev: v2.3.2 hooks: - id: prettier exclude: ^Pipfile\.lock diff --git a/Pipfile b/Pipfile index c9238d3f69e..7f386c5381f 100644 --- a/Pipfile +++ b/Pipfile @@ -4,23 +4,38 @@ url = "https://pypi.python.org/simple" verify_ssl = true [dev-packages] -Sphinx = ">=3.1.2" -coverage = "*" -docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 -flake8 = "*" +# 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. +pre-commit = ">=2.9.2" +flake8 = ">=3.9.2" flake8-bugbear = "*" -mypy = ">=0.812" +mypy = ">=0.910" types-dataclasses = ">=0.1.3" types-typed-ast = ">=1.4.1" -pre-commit = "*" -readme_renderer = "*" -MyST-Parser = ">=0.13.7" + +# Documentation related requirements. +Sphinx = ">=4.1.2" +MyST-Parser = ">=0.15.1" sphinxcontrib-programoutput = ">=0.17" -sphinx-copybutton = ">=0.3.0" +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] diff --git a/Pipfile.lock b/Pipfile.lock index 0c9a42348c3..6d62ec76680 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eca7ef453dc0d16ab3910fc8035a3b57e2c1512dea8d9a95958eeecfe71935c1" + "sha256": "ac07cc9a5cb19ea72381baf4ba0db1689f475538d37e4be3119fc958a722b062" }, "pipfile-spec": 6, "requires": {}, @@ -106,12 +106,12 @@ }, "dataclasses": { "hashes": [ - "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", - "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" + "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", + "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" ], "index": "pypi", "python_version <": "3.7", - "version": "==0.6", + "version": "==0.8", "version >": "0.1.3" }, "idna": { @@ -122,6 +122,21 @@ "markers": "python_version >= '3.5'", "version": "==3.2" }, + "idna-ssl": { + "hashes": [ + "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "markers": "python_version < '3.7'", + "version": "==1.1.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f", + "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5" + ], + "markers": "python_version < '3.8'", + "version": "==4.6.4" + }, "multidict": { "hashes": [ "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", @@ -191,42 +206,50 @@ }, "regex": { "hashes": [ - "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b", - "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16", - "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da", - "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d", - "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba", - "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1", - "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c", - "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281", - "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576", - "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83", - "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39", - "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3", - "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee", - "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce", - "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20", - "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9", - "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a", - "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6", - "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d", - "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d", - "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b", - "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d", - "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16", - "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363", - "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f", - "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a", - "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91", - "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80", - "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531", - "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b", - "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6", - "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c", - "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6" + "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" ], "index": "pypi", - "version": "==2021.8.3" + "version": "==2021.8.21" }, "setuptools-scm": { "hashes": [ @@ -333,6 +356,14 @@ ], "markers": "python_version >= '3.6'", "version": "==1.6.3" + }, + "zipp": { + "hashes": [ + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" } }, "develop": { @@ -394,14 +425,6 @@ ], "version": "==0.7.12" }, - "appnope": { - "hashes": [ - "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", - "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.2" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -463,6 +486,56 @@ ], "version": "==2021.5.30" }, + "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" + }, "cfgv": { "hashes": [ "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1", @@ -561,6 +634,43 @@ "index": "pypi", "version": "==5.5" }, + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.7" + }, + "dataclasses": { + "hashes": [ + "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", + "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" + }, "decorator": { "hashes": [ "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323", @@ -578,12 +688,19 @@ }, "docutils": { "hashes": [ - "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", - "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521", - "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" ], "index": "pypi", - "version": "==0.15" + "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": [ @@ -624,6 +741,13 @@ "markers": "python_version >= '3.5'", "version": "==3.2" }, + "idna-ssl": { + "hashes": [ + "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "markers": "python_version < '3.7'", + "version": "==1.1.0" + }, "imagesize": { "hashes": [ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", @@ -634,19 +758,34 @@ }, "importlib-metadata": { "hashes": [ - "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9", - "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b" + "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f", + "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5" ], - "markers": "python_version >= '3.6'", - "version": "==4.6.3" + "markers": "python_version < '3.8'", + "version": "==4.6.4" + }, + "importlib-resources": { + "hashes": [ + "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977", + "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b" + ], + "markers": "python_version < '3.7' and python_version < '3.7'", + "version": "==5.2.2" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" }, "ipython": { "hashes": [ - "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e", - "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362" + "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64", + "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf" ], - "markers": "python_version >= '3.7'", - "version": "==7.26.0" + "markers": "python_version >= '3.6'", + "version": "==7.16.1" }, "ipython-genutils": { "hashes": [ @@ -663,6 +802,14 @@ "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:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", @@ -673,11 +820,18 @@ }, "keyring": { "hashes": [ - "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", - "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" + "sha256:b32397fd7e7063f8dd74a26db910c9862fc2109285fa16e3b5208bcb42a3e579", + "sha256:b7e0156667f5dcc73c1f63a518005cd18a4eb23fe77321194fefcc03748b21a4" ], "markers": "python_version >= '3.6'", - "version": "==23.0.1" + "version": "==23.1.0" + }, + "makefun": { + "hashes": [ + "sha256:033eed65e2c1804fca84161a38d1fc8bb8650d32a89ac1c5dc7e54b2b2c2e88c", + "sha256:a19bddf07efb6bf92e3ccde5d593e49bc59001fd6c17cf7301d7a73a2647ae83" + ], + "version": "==1.11.3" }, "markdown-it-py": { "hashes": [ @@ -694,30 +848,50 @@ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -727,14 +901,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", @@ -853,6 +1019,14 @@ "markers": "python_version >= '3.6'", "version": "==21.0" }, + "parameterized": { + "hashes": [ + "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c", + "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9" + ], + "index": "pypi", + "version": "==0.8.1" + }, "parso": { "hashes": [ "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", @@ -899,6 +1073,14 @@ "index": "pypi", "version": "==2.2.0" }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, "pre-commit": { "hashes": [ "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c", @@ -909,11 +1091,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", - "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" + "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c", + "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.0.19" + "markers": "python_full_version >= '3.6.2'", + "version": "==3.0.20" }, "ptyprocess": { "hashes": [ @@ -922,6 +1104,14 @@ ], "version": "==0.7.0" }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, "pycodestyle": { "hashes": [ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", @@ -930,6 +1120,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", @@ -940,20 +1138,68 @@ }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], "markers": "python_version >= '3.5'", - "version": "==2.9.0" + "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'", + "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" + ], + "index": "pypi", + "version": "==6.2.4" + }, + "pytest-cases": { + "hashes": [ + "sha256:13136269240615bc79041f8af8fc96e0e3e085da72dd22b18625451fda2443b8", + "sha256:a4abe0ec2b8acf8f8b5ab73060de72eac745c6ed9cfa317d59ae71b4a0bbbdf5" + ], + "index": "pypi", + "version": "==3.6.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", + "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + ], + "index": "pypi", + "version": "==2.12.1" + }, + "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-mock": { + "hashes": [ + "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3", + "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62" + ], + "index": "pypi", + "version": "==3.6.1" + }, + "pytest-xdist": { + "hashes": [ + "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5", + "sha256:ed3d7da961070fce2a01818b51f6888327fb88df4379edeb6b9d990e789d9c8d" + ], + "index": "pypi", + "version": "==2.3.0" + }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -1006,42 +1252,50 @@ }, "regex": { "hashes": [ - "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b", - "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16", - "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da", - "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d", - "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba", - "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1", - "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c", - "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281", - "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576", - "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83", - "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39", - "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3", - "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee", - "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce", - "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20", - "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9", - "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a", - "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6", - "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d", - "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d", - "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b", - "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d", - "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16", - "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363", - "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f", - "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a", - "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91", - "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80", - "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531", - "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b", - "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6", - "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c", - "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6" + "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" ], "index": "pypi", - "version": "==2021.8.3" + "version": "==2021.8.21" }, "requests": { "hashes": [ @@ -1065,6 +1319,14 @@ ], "version": "==1.5.0" }, + "secretstorage": { + "hashes": [ + "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", + "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.1" + }, "setuptools-scm": { "hashes": [ "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", @@ -1078,7 +1340,7 @@ "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": { @@ -1173,7 +1435,7 @@ "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": { @@ -1184,21 +1446,28 @@ "index": "pypi", "version": "==1.2.1" }, + "tox": { + "hashes": [ + "sha256:9fbf8e2ab758b2a5e7cb2c72945e4728089934853076f67ef18d7575c8ab6b88", + "sha256:c6c4e77705ada004283610fd6d9ba4f77bc85d235447f875df9f0ba1bc23b634" + ], + "index": "pypi", + "version": "==3.24.3" + }, "tqdm": { "hashes": [ - "sha256:3642d483b558eec80d3c831e23953582c34d7e4540db86d9e5ed9dad238dabc6", - "sha256:706dea48ee05ba16e936ee91cb3791cd2ea6da348a0e50b46863ff4363ff4340" + "sha256:07856e19a1fe4d2d9621b539d3f072fa88c9c1ef1f3b7dd4d4953383134c3164", + "sha256:35540feeaca9ac40c304e916729e6b78045cbbeccd3e941b2868f09306798ac9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.0" + "version": "==4.62.1" }, "traitlets": { "hashes": [ - "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", - "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" + "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", + "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" ], - "markers": "python_version >= '3.7'", - "version": "==5.0.5" + "version": "==4.3.3" }, "twine": { "hashes": [ @@ -1208,6 +1477,42 @@ "index": "pypi", "version": "==3.4.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" + ], + "index": "pypi", + "version": "==1.4.2" + }, "types-dataclasses": { "hashes": [ "sha256:248075d093d8f7c1541ce515594df7ae40233d1340afde11ce7125368c5209b8", @@ -1245,11 +1550,11 @@ }, "virtualenv": { "hashes": [ - "sha256:57bcb59c5898818bd555b1e0cfcf668bd6204bc2b53ad0e70a52413bd790f9e4", - "sha256:73863dc3be1efe6ee638e77495c0c195a6384ae7b15c561f3ceb2698ae7267c1" + "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0", + "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.7.1" + "version": "==20.7.2" }, "wcwidth": { "hashes": [ diff --git a/docs/requirements.txt b/docs/requirements.txt index 56dbf3ffe13..4c5b700412a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Used by ReadTheDocs; pinned requirements for stability. -MyST-Parser==0.14.0 -Sphinx==3.5.4 +myst-parser==0.15.1 +Sphinx==4.1.2 sphinxcontrib-programoutput==0.17 -sphinx_copybutton==0.3.1 +sphinx_copybutton==0.4.0 From 0969ca4a46c4a2081be38f7e96a81a74b308c75f Mon Sep 17 00:00:00 2001 From: erykoff Date: Tue, 24 Aug 2021 13:59:24 -0700 Subject: [PATCH 329/680] Change sys.exit to raise ImportError (#2440) The fix for #1688 in #1761 breaks help("modules") introspection and also leads to unhappy results when inadvertently importing blackd from Python. Basically the sys.exit(-1) causes the whole Python REPL to exit -- not great to suffice. Commit history before merge: * Change sys.exit to Raise. * Add #2440 to changelog. * Fix lint error from prettier * Remove exception chain for more helpful user message. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- CHANGES.md | 4 ++++ src/blackd/__init__.py | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 22ddc423e55..ed08ab3b9ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,10 @@ - The failsafe for accidentally added backslashes in f-string expressions has been hardened to handle more edge cases during quote normalization (#2437) +### _Blackd_ + +- Replace sys.exit(-1) with raise ImportError (#2440) + ### Integrations - The provided pre-commit hooks no longer specify `language_version` to avoid overriding diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 3e2a7e7c30f..5fdec152226 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -1,6 +1,5 @@ import asyncio import logging -import sys from concurrent.futures import Executor, ProcessPoolExecutor from datetime import datetime from functools import partial @@ -11,13 +10,11 @@ from aiohttp import web import aiohttp_cors except ImportError as ie: - print( + raise ImportError( f"aiohttp dependency is not installed: {ie}. " + "Please re-install black with the '[d]' extra install " - + "to obtain aiohttp_cors: `pip install black[d]`", - file=sys.stderr, - ) - sys.exit(-1) + + "to obtain aiohttp_cors: `pip install black[d]`" + ) from None import black from black.concurrency import maybe_install_uvloop From 5bb4da02c2c8c92d017e8b6be57eb442cc8f04ff Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 24 Aug 2021 14:29:49 -0700 Subject: [PATCH 330/680] Add cpython Lib/ repository config into primer config - Disabled (#2429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CPython repository into primer runs - CPython tests is probably the best repo for black to test on as the stdlib's unittests should use all syntax - Limit to running in recent versions of the python runtime - e.g. today >= 3.9 - This allows us to parse more syntax - Exclude all failing files for now - Definitely have bugs to explore there - Refer to #2407 for more details there - Some test files on purpose have syntax errors, so we will never be able to parse them - Add new black command arguments logging in debug mode; very handy for seeing how CLI arguments are formatted CPython now succeeds ignoring 16 files: ``` Oh no! 💥 💔 💥 1859 files would be reformatted, 148 files would be left unchanged. ``` Testing - Ran locally with and without string processing - Very little runtime difference BUT 3 more failed files ``` time /tmp/tb/bin/black --experimental-string-processing --check . 2>&1 | tee /tmp/black_cpython_esp ... Oh no! 💥 💔 💥 1859 files would be reformatted, 148 files would be left unchanged, 16 files would fail to reformat. real 4m8.563s user 16m21.735s sys 0m6.000s ``` - Add unittest for new covienence config file flattening that allows long arguments to be broke up into an array/list of strings Addresses #2407 --- Commit history before merge: * Add new `timeout_seconds` support into primer.json - If present, will set forked process limit to that value in seconds - Otherwise, stay with default 10 minutes (600 seconds) * Add new "base_path" concept to black-primer - Rather than start at the repo root start at a configured path within the repository - e.g. for cpython only run black on `Lib` * Disable by default - It's too much for GitHub Actions. But let's leave config for others to use * Minor tweak to _flatten_cli_args Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- src/black_primer/lib.py | 42 ++++++++++++++++++++++++++++++++---- src/black_primer/primer.json | 33 +++++++++++++++++++++++++++- tests/test_primer.py | 7 ++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/black_primer/lib.py b/src/black_primer/lib.py index 784134bf652..7494ae6dc7d 100644 --- a/src/black_primer/lib.py +++ b/src/black_primer/lib.py @@ -12,12 +12,23 @@ from subprocess import CalledProcessError from sys import version_info from tempfile import TemporaryDirectory -from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence, Tuple +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" @@ -39,7 +50,7 @@ class Results(NamedTuple): async def _gen_check_output( cmd: Sequence[str], - timeout: float = 600, + timeout: float = TEN_MINUTES_SECONDS, env: Optional[Dict[str, str]] = None, cwd: Optional[Path] = None, stdin: Optional[bytes] = None, @@ -113,6 +124,21 @@ def analyze_results(project_count: int, results: Results) -> int: 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], @@ -131,7 +157,7 @@ async def black_run( stdin_test = project_name.upper() == "STDIN" cmd = [str(which(BLACK_BINARY))] if "cli_arguments" in project_config and project_config["cli_arguments"]: - cmd.extend(project_config["cli_arguments"]) + cmd.extend(_flatten_cli_args(project_config["cli_arguments"])) cmd.append("--check") if not no_diff: cmd.append("--diff") @@ -141,9 +167,16 @@ async def black_run( 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 = { @@ -154,8 +187,9 @@ async def black_run( 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 + cmd, cwd=cwd_path, env=env, stdin=stdin, timeout=timeout ) except asyncio.TimeoutError: results.stats["failed"] += 1 diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index edbed3f33dd..0d1018fc50e 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -1,5 +1,5 @@ { - "configuration_format_version": 20200509, + "configuration_format_version": 20210815, "projects": { "STDIN": { "cli_arguments": ["--experimental-string-processing"], @@ -36,6 +36,37 @@ "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", diff --git a/tests/test_primer.py b/tests/test_primer.py index 8dd1212313e..e7f99fdb0c2 100644 --- a/tests/test_primer.py +++ b/tests/test_primer.py @@ -146,6 +146,11 @@ def test_black_run(self) -> None: ) 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() @@ -184,6 +189,8 @@ def test_git_checkout_or_rebase(self) -> None: @patch("sys.stdout", new_callable=StringIO) @event_loop() def test_process_queue(self, mock_stdout: Mock) -> None: + """Test the process queue on primer itself + - If you have non black conforming formatting in primer itself this can fail""" loop = asyncio.get_event_loop() config_path = Path(lib.__file__).parent / "primer.json" with patch("black_primer.lib.git_checkout_or_rebase", return_false): From d249f2d8380893f051b63140e3768cef599f49f3 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 25 Aug 2021 21:25:44 -0400 Subject: [PATCH 331/680] MNT: add pull request template (#2443) So we don't have to request changes on these basic requirements as often - hopefully :) --- .github/PULL_REQUEST_TEMPLATE.md | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..36f98c527f4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + + +### Description + + + +### Checklist - did you ... + + + +- [] Add a CHANGELOG entry if necessary? +- [] Add / update tests if necessary? +- [] Add new / update outdated documentation? + + From 8a59528c2d8ae1ef5f366039c728614aaf1a470b Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 25 Aug 2021 21:32:27 -0400 Subject: [PATCH 332/680] Stop changing return type annotations to tuples (#2384) This fixes a bug where a trailing comma would be added to a parenthesized return annotation changing its type to a tuple. Here's one case where this bug shows up: ``` def spam() -> ( this_is_a_long_type_annotation_which_should_NOT_get_a_trailing_comma ): pass ``` The root problem was that the type annotation was treated as if it was a parameter & import list (is_body=True to linegen::bracket_split_build_line) where a trailing comma is usually fine. Now there's another check in the aforementioned function to make sure the body it's operating on isn't a return annotation before truly adding a trailing comma. --- CHANGES.md | 2 + src/black/linegen.py | 16 ++++++- src/black/nodes.py | 2 + tests/data/function_trailing_comma.py | 64 ++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ed08ab3b9ad..47e64cfa274 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,8 @@ - Present a more user-friendly error if .gitignore is invalid (#2414) - The failsafe for accidentally added backslashes in f-string expressions has been hardened to handle more edge cases during quote normalization (#2437) +- Avoid changing a function return type annotation's type to a tuple by adding a + trailing comma (#2384) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 76b553a959a..fafaf1032ca 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field -from black.nodes import WHITESPACE, STATEMENT, STANDALONE_COMMENT +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 is_docstring, is_empty_tuple, is_one_tuple, is_one_tuple_between @@ -574,6 +574,20 @@ def bracket_split_build_line( original.is_def and opening_bracket.value == "(" and not any(leaf.type == token.COMMA 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 ... + # and so on depending on how complex the return annotation is. + # This isn't perfect and there's some false negatives but they are in + # contexts were a comma is actually fine. + and not any( + node.prev_sibling.type == RARROW + for node in ( + leaves[0].parent, + getattr(leaves[0].parent, "parent", None), + ) + if isinstance(node, Node) and isinstance(node.prev_sibling, Leaf) + ) ) if original.is_import or no_commas: diff --git a/src/black/nodes.py b/src/black/nodes.py index e0db9a42426..8f2e15b2cc3 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -135,6 +135,8 @@ BRACKETS = OPENING_BRACKETS | CLOSING_BRACKETS ALWAYS_NO_SPACE = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT} +RARROW = 55 + class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index d15459cbeb5..02078219e82 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -21,6 +21,34 @@ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ ]: json = {"k": {"k2": {"k3": [1,]}}} + + +# The type annotation shouldn't get a trailing comma since that would change its type. +# Relevant bug report: https://github.com/psf/black/issues/2381. +def some_function_with_a_really_long_name() -> ( + returning_a_deeply_nested_import_of_a_type_i_suppose +): + pass + + +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 +): + pass + + +def func() -> ( + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) +): + pass + + +def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too + )) +): + pass + # output def f( @@ -85,4 +113,38 @@ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ ] } } - } \ No newline at end of file + } + + +# The type annotation shouldn't get a trailing comma since that would change its type. +# Relevant bug report: https://github.com/psf/black/issues/2381. +def some_function_with_a_really_long_name() -> ( + returning_a_deeply_nested_import_of_a_type_i_suppose +): + pass + + +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 +): + pass + + +def func() -> ( + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too + ) +): + pass + + +def func() -> ( + ( + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( + this_shouldn_t_get_a_trailing_comma_too + ) + ) +): + pass From 366a0806eb4edd94d4d20cc6912333586bdc9fa7 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 26 Aug 2021 16:59:01 -0400 Subject: [PATCH 333/680] blib2to3: support unparenthesized wulruses in more places (#2447) Implementation stolen from PR davidhalter/parso#162. Thanks parso! I could add support for these newer syntactical constructs in the target version detection logic, but until I get diff-shades up and running I don't feel very comfortable adding the code. --- CHANGES.md | 2 ++ src/blib2to3/Grammar.txt | 6 +++--- src/blib2to3/README | 7 ++++++- tests/data/pep_572_py310.py | 4 ++++ tests/data/pep_572_py39.py | 7 +++++++ tests/test_black.py | 9 +++++++++ 6 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 tests/data/pep_572_py310.py create mode 100644 tests/data/pep_572_py39.py diff --git a/CHANGES.md b/CHANGES.md index 47e64cfa274..d28d766f4c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,8 @@ hardened to handle more edge cases during quote normalization (#2437) - Avoid changing a function return type annotation's type to a tuple by adding a trailing comma (#2384) +- Parsing support has been added for unparenthesized walruses in set literals, set + comprehensions, and indices (#2447). ### _Blackd_ diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 69b9af96608..ac8a067378d 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -157,14 +157,14 @@ testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test| lambdef: 'lambda' [varargslist] ':' test trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] -subscript: test | [test] ':' [test] [sliceop] +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))* [','])) | - ((test | star_expr) - (comp_for | (',' (test | star_expr))* [','])) ) + ((test [':=' test] | star_expr) + (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) classdef: 'class' NAME ['(' [arglist] ')'] ':' suite diff --git a/src/blib2to3/README b/src/blib2to3/README index a43f15cb37d..ccad28337b6 100644 --- a/src/blib2to3/README +++ b/src/blib2to3/README @@ -13,4 +13,9 @@ Reasons for forking: - ability to Cythonize Change Log: -- Changes default logger used by Driver \ No newline at end of file +- Changes default logger used by Driver +- Backported the following upstream parser changes: + - "bpo-42381: Allow walrus in set literals and set comprehensions (GH-23332)" + https://github.com/python/cpython/commit/cae60187cf7a7b26281d012e1952fafe4e2e97e9 + - "bpo-42316: Allow unparenthesized walrus operator in indexes (GH-23317)" + https://github.com/python/cpython/commit/b0aba1fcdc3da952698d99aec2334faa79a8b68c diff --git a/tests/data/pep_572_py310.py b/tests/data/pep_572_py310.py new file mode 100644 index 00000000000..2aef589ce8d --- /dev/null +++ b/tests/data/pep_572_py310.py @@ -0,0 +1,4 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] diff --git a/tests/data/pep_572_py39.py b/tests/data/pep_572_py39.py new file mode 100644 index 00000000000..7bbd5091197 --- /dev/null +++ b/tests/data/pep_572_py39.py @@ -0,0 +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)} +# 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/test_black.py b/tests/test_black.py index 5c720507216..8a37f7c65b4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -26,6 +26,7 @@ import pytest import unittest from unittest.mock import patch, MagicMock +from parameterized import parameterized import click from click import unstyle @@ -299,6 +300,14 @@ 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)) From 8b0680533420c2ea367860fcbb08df99317a6b44 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Fri, 27 Aug 2021 21:21:08 +0100 Subject: [PATCH 334/680] Document jupyter hook (#2416) This also introduces a script so we can reference the latest version in the example pre-commit configuration in the docs without forgetting to update it when doing a release! Commit history before merge: * document jupyter hook * note minimum version * add check for pre-commit version * use git tag * curl api during ci * parse version from changes file * fixup script * rename variables * Tweak the docs & magical script * fix couple of typos * pin additional dependencies in hook * Add types-PyYAML to lockfile Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .pre-commit-config.yaml | 9 ++++ Pipfile | 1 + Pipfile.lock | 24 +++++++-- docs/faq.md | 2 +- docs/integrations/source_version_control.md | 15 +++++- scripts/__init__.py | 0 scripts/check_pre_commit_rev_in_example.py | 54 +++++++++++++++++++++ src/black/__init__.py | 2 +- 8 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 scripts/__init__.py create mode 100644 scripts/check_pre_commit_rev_in_example.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ae418d9bdd..b3cbe352e38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,14 @@ repos: require_serial: true types_or: [python, pyi] + - id: check-pre-commit-rev-in-example + name: Check pre-commit rev in example + language: python + 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"] + - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: @@ -25,6 +33,7 @@ repos: 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.0.0 diff --git a/Pipfile b/Pipfile index 7f386c5381f..824beda16cc 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ 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" diff --git a/Pipfile.lock b/Pipfile.lock index 6d62ec76680..30a4defcca3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ac07cc9a5cb19ea72381baf4ba0db1689f475538d37e4be3119fc958a722b062" + "sha256": "ebf216584cfb2c962a1792d0682f3c08b44c7ae27305a03a54eacd6f42df27db" }, "pipfile-spec": 6, "requires": {}, @@ -901,6 +901,14 @@ "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", @@ -1149,7 +1157,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": { @@ -1340,7 +1348,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": { @@ -1435,7 +1443,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": { @@ -1521,6 +1529,14 @@ "index": "pypi", "version": "==0.1.7" }, + "types-pyyaml": { + "hashes": [ + "sha256:745dcb4b1522423026bcc83abb9925fba747f1e8602d902f71a4058f9e7fb662", + "sha256:96f8d3d96aa1a18a465e8f6a220e02cff2f52632314845a364ecbacb0aea6e30" + ], + "index": "pypi", + "version": "==5.4.6" + }, "types-typed-ast": { "hashes": [ "sha256:b7f561796b4d002c7522b0020f58b18f715bd28a31429d424a78e2e2dbbd6785", diff --git a/docs/faq.md b/docs/faq.md index d7e6a16351f..c361addf7ae 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -57,7 +57,7 @@ following will not be formatted: get_ipython().system('ls') ``` -- invalid syntax, as it can't be safely distinguished from automagics in the absense of +- invalid syntax, as it can't be safely distinguished from automagics in the absence of a running `IPython` kernel. ## Why are Flake8's E203 and W503 violated? diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 1ca6161bf0b..e1482487aa4 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -7,8 +7,21 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: stable # Replace by any tag/version: https://github.com/psf/black/tags + rev: 21.7b0 hooks: - id: black language_version: python3 # Should be a command that runs python3.6+ ``` + +Feel free to switch out the `rev` value to something else, like another +[tag/version][black-tags] or even a specific commit. Although we discourage the use of +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). + +[black-tags]: https://github.com/psf/black/tags +[pre-commit-mutable-rev]: + https://pre-commit.com/#using-the-latest-version-for-a-repository diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/check_pre_commit_rev_in_example.py b/scripts/check_pre_commit_rev_in_example.py new file mode 100644 index 00000000000..9560b3b8401 --- /dev/null +++ b/scripts/check_pre_commit_rev_in_example.py @@ -0,0 +1,54 @@ +""" +Check that the rev value in the example pre-commit configuration matches +the latest version of Black. This saves us from forgetting to update that +during the release process. + +Why can't we just use `rev: stable` and call it a day? Well pre-commit +won't auto update the hook as you may expect (and for good reasons, some +technical and some pragmatic). Encouraging bad practice is also just +not ideal. xref: https://github.com/psf/black/issues/420 +""" + +import os +import sys + +import commonmark +import yaml +from bs4 import BeautifulSoup + + +def main(changes: str, source_version_control: str) -> None: + changes_html = commonmark.commonmark(changes) + changes_soup = BeautifulSoup(changes_html, "html.parser") + headers = changes_soup.find_all("h2") + latest_tag, *_ = [ + header.string for header in headers if header.string != "Unreleased" + ] + + source_version_control_html = commonmark.commonmark(source_version_control) + source_version_control_soup = BeautifulSoup( + source_version_control_html, "html.parser" + ) + pre_commit_repos = yaml.safe_load( + source_version_control_soup.find(class_="language-yaml").string + )["repos"] + + for repo in pre_commit_repos: + pre_commit_rev = repo["rev"] + if not pre_commit_rev == latest_tag: + print( + "Please set the rev in ``source_version_control.md`` to be the latest " + f"one.\nExpected {latest_tag}, got {pre_commit_rev}.\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", "integrations", "source_version_control.md"), + encoding="utf-8", + ) as fd: + source_version_control = fd.read() + main(changes, source_version_control) diff --git a/src/black/__init__.py b/src/black/__init__.py index 60f4fa34e9d..d033e01141a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -977,7 +977,7 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon """Format Jupyter notebook. Operate cell-by-cell, only on code cells, only for Python notebooks. - If the ``.ipynb`` originally had a trailing newline, it'll be preseved. + If the ``.ipynb`` originally had a trailing newline, it'll be preserved. """ trailing_newline = src_contents[-1] == "\n" modified = False From fcfead919b352e9530de4c198ae03f424c80e3ca Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 28 Aug 2021 14:55:34 +0100 Subject: [PATCH 335/680] set mypy_path in mypy.ini (#2455) --- mypy.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy.ini b/mypy.ini index ae7be5e5106..7e563e6f696 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,6 +5,8 @@ python_version=3.6 platform=linux +mypy_path=src + show_column_numbers=True # show error messages from unrelated files From 7a093f0303c3fa7fee0b6742b7c6a3ff03438ace Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Sat, 28 Aug 2021 16:27:55 +0100 Subject: [PATCH 336/680] add test which covers stdin filename ipynb (#2454) --- tests/test_black.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_black.py b/tests/test_black.py index 8a37f7c65b4..398a528bee9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1639,6 +1639,30 @@ 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) + def test_reformat_one_with_stdin_filename_ipynb(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + p = "foo.ipynb" + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once_with( + fast=True, + write_back=black.WriteBack.YES, + mode=replace(DEFAULT_MODE, is_ipynb=True), + ) + # __BLACK_STDIN_FILENAME__ should have been stripped + report.done.assert_called_with(expected, black.Changed.YES) + def test_reformat_one_with_stdin_and_existing_path(self) -> None: with patch( "black.format_stdin_to_stdout", From a5bb6e0a320bf42f1b219ee2e55ea1c1917961dc Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 28 Aug 2021 15:37:53 -0400 Subject: [PATCH 337/680] Pin setuptools-scm build time dependency (#2457) The setuptools-scm dependency in setup.cfg did not have a version specified, leading to the issues described in #2449 after a faulty release of setuptools-scm was published. To avoid this issue in the future, the last version before that faulty update is now pinned. Commit history before merge: * Pin setuptools-scm dependency version (#2449) * Update CHANGES.md * Let's pin in pyproject.toml too Mostly since it's non-build-backend specific configuration and more widely standardized file. Not sure what benefits pinning in setup.cfg gives us on top of pyproject.toml but I'd rather not find out during the release that is supposed to happen today :wink: Co-authored-by: FiNs <24248249+FabianNiehaus@users.noreply.github.com> --- CHANGES.md | 1 + pyproject.toml | 5 ++++- setup.cfg | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d28d766f4c0..6c542f446e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ trailing comma (#2384) - Parsing support has been added for unparenthesized walruses in set literals, set comprehensions, and indices (#2447). +- Pin `setuptools-scm` build-time dependency version (#2457) ### _Blackd_ diff --git a/pyproject.toml b/pyproject.toml index d085c0ddc62..30e62974475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,10 @@ extend-exclude = ''' # NOTE: You don't need this in your own Black configuration. [build-system] -requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] +# We're pinning setuptools-scm to bugfix versions only because for build-time +# deps having them work on install by default is really important. Especially +# since it's hard for users to work-around the specified build requirements. +requires = ["setuptools>=41.0", "setuptools_scm~=6.0.1", "wheel"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] diff --git a/setup.cfg b/setup.cfg index 55c66add7fb..dbd667e8f19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,3 @@ [options] -setup_requires = setuptools_scm +setup_requires = + setuptools_scm~=6.0.1 From 16275d24e3c2ad88d953d67e7d8795fd7f73f7cb Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:56:22 -0400 Subject: [PATCH 338/680] Prepare CHANGES.md for release 21.8b0 (#2458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hopefully my first release doesn't end up in flames 🔥 Commit history before merge: * Prepare CHANGES.md for release 21.8b0 * I need to add a check for this too. --- CHANGES.md | 5 +++-- docs/integrations/source_version_control.md | 2 +- docs/usage_and_configuration/the_basics.md | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6c542f446e7..6e2721ba19c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 21.8b0 ### _Black_ @@ -17,7 +17,8 @@ ### _Blackd_ -- Replace sys.exit(-1) with raise ImportError (#2440) +- Replace sys.exit(-1) with raise ImportError as it plays more nicely with tools that + scan installed packages (#2440) ### Integrations diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index e1482487aa4..220f429eb20 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.7b0 + rev: 21.8b0 hooks: - id: black language_version: python3 # Should be a command that runs python3.6+ diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 474ad669cd1..4b68ad7b76b 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -173,13 +173,13 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, version 21.5b0 +black, version 21.8b0 ``` An option to require a specific version to be running is also provided. ```console -$ black --required-version 21.5b2 -c "format = 'this'" +$ black --required-version 21.8b0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! From a8b4665e7d6eb945c47820adb1a3f8b006adce0c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 29 Aug 2021 17:04:49 -0400 Subject: [PATCH 339/680] Exclude broken typing-extensions version + fix import (#2460) re. import, the ipynb code was assuming that typing-extensions would always be available, but that's not the case! There's an environment marker on the requirement meaning it won't get installed on 3.10 or higher. The test suite didn't catch this issue since aiohttp pulls in typing-extensions unconditionally. --- CHANGES.md | 2 ++ setup.py | 5 ++++- src/black/handle_ipynb_magics.py | 12 ++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6e2721ba19c..576e3c84882 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ - Parsing support has been added for unparenthesized walruses in set literals, set comprehensions, and indices (#2447). - Pin `setuptools-scm` build-time dependency version (#2457) +- Exclude typing-extensions version 3.10.0.1 due to it being broken on Python 3.10 + (#2460) ### _Blackd_ diff --git a/setup.py b/setup.py index 215fa6cff61..929096a2098 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,10 @@ def get_long_description() -> str: "regex>=2020.1.8", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", - "typing_extensions>=3.10.0.0; python_version < '3.10'", + "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'", "mypy_extensions>=0.4.3", ], extras_require={ diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index ad93c444efc..b18f8629136 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -1,15 +1,19 @@ """Functions to process IPython magics with.""" + from functools import lru_cache import dataclasses import ast -from typing import Dict +from typing import Dict, List, Tuple, Optional import secrets -from typing import List, Tuple +import sys import collections -from typing import Optional -from typing_extensions import TypeGuard +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 79575f3376f043186d8b8c4885ef51c6b3c36246 Mon Sep 17 00:00:00 2001 From: aru Date: Mon, 30 Aug 2021 13:26:21 -0400 Subject: [PATCH 340/680] fix: run pypi / docker upload from published draft releases (#2461) Draft releases don't trigger the workflows (that's good!) but since they only Commit history before merge: * fix: run pypi upload from published draft releases * Fix broken task list markup in PR template * change docker workflow to build on release publish Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++--- .github/workflows/docker.yml | 2 +- .github/workflows/pypi_upload.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 36f98c527f4..833cd164134 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,9 +18,9 @@ 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 / update tests if necessary? -- [] Add new / update outdated documentation? +- [ ] Add a CHANGELOG entry if necessary? +- [ ] Add / update tests if necessary? +- [ ] Add new / update outdated documentation? + **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 372/680] 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 373/680] 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 374/680] 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 375/680] 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 376/680] 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 377/680] 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 378/680] 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 379/680] 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 380/680] 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 381/680] 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 382/680] 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 383/680] 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 384/680] 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 385/680] 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 386/680] 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 387/680] 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 388/680] 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 389/680] 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 390/680] 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 391/680] 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 392/680] 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 393/680] 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 394/680] 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 395/680] 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 396/680] 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 397/680] 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 398/680] 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 399/680] 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 400/680] 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 401/680] 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 402/680] 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 403/680] 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 404/680] 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 405/680] 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 406/680] 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 407/680] 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 408/680] 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 409/680] 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 410/680] 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 411/680] 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 412/680] 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 413/680] 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 414/680] 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 415/680] 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 416/680] 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 417/680] 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 418/680] 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 419/680] 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 420/680] 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 421/680] 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 422/680] 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 423/680] 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 424/680] 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 425/680] 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 426/680] 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 427/680] `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 428/680] 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 429/680] 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 430/680] 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 431/680] 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 432/680] 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 433/680] 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 434/680] 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 435/680] 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 436/680] 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 437/680] 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 438/680] 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 439/680] 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 440/680] 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 441/680] 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 442/680] 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 443/680] 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 444/680] 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 445/680] 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 446/680] 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 447/680] 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 448/680] 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 449/680] 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 450/680] 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 451/680] 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 452/680] 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 453/680] 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 454/680] 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 455/680] 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 456/680] 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 457/680] 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 458/680] 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 459/680] 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 460/680] 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 461/680] 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 462/680] 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 463/680] 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 464/680] 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 465/680] [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 466/680] 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 467/680] 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 468/680] 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 469/680] 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 470/680] 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 471/680] 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 472/680] 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 473/680] 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 474/680] 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 475/680] 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 476/680] 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 477/680] 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 478/680] 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 479/680] 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 480/680] 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 481/680] 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 482/680] 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 483/680] 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 484/680] 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 485/680] 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 486/680] 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 487/680] 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 488/680] 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 489/680] 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 490/680] 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 491/680] 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 492/680] 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 493/680] 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 494/680] 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 495/680] 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 496/680] 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 497/680] 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 498/680] 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 499/680] 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 500/680] 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 501/680] 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 502/680] 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 503/680] 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 504/680] 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 505/680] 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 506/680] 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 507/680] 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 508/680] 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 509/680] 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 510/680] 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 511/680] 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 512/680] 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 513/680] 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 514/680] 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 515/680] 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 516/680] 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 517/680] 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 518/680] 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 519/680] 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 520/680] 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 521/680] 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 522/680] 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 523/680] 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 524/680] 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 525/680] 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 526/680] 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 527/680] 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 528/680] 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 529/680] 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 530/680] 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 531/680] 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 532/680] 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 533/680] 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 534/680] 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 535/680] 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 536/680] 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 537/680] 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 538/680] 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 539/680] 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 540/680] 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 541/680] 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 542/680] 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 543/680] 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 544/680] 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 545/680] 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 546/680] 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 547/680] 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 548/680] 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 549/680] 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 550/680] 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 551/680] 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 552/680] 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 553/680] 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 554/680] 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 555/680] 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 556/680] 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 557/680] 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 558/680] 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 559/680] 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 560/680] 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 561/680] 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 562/680] 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 563/680] 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 564/680] 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 565/680] 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 566/680] 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 567/680] 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 568/680] 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 569/680] 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 570/680] 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 571/680] 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 572/680] 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 573/680] 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 574/680] 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 575/680] 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 576/680] 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 577/680] 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 578/680] 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 579/680] 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 580/680] 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 581/680] 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 582/680] 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 583/680] 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 584/680] 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 585/680] 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 586/680] 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 587/680] 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 588/680] 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 589/680] 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 590/680] 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 591/680] 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 592/680] 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 593/680] 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 594/680] 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 595/680] 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 596/680] 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 597/680] 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 598/680] 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 599/680] 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 600/680] 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 601/680] 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 602/680] 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 603/680] 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 604/680] 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 605/680] 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 606/680] 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 607/680] 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 608/680] 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 609/680] 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 610/680] 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 611/680] 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 612/680] 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 613/680] 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 614/680] 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 615/680] 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 616/680] 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 617/680] 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 618/680] 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 619/680] 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 620/680] 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 621/680] 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 622/680] 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 623/680] 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 624/680] 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 625/680] 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 626/680] 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 627/680] 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 628/680] 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 629/680] 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 630/680] 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 631/680] 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 632/680] 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 635/680] 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 636/680] 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 637/680] 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 638/680] 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 639/680] 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 640/680] 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 641/680] 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 642/680] 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 643/680] 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 644/680] 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 645/680] 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 646/680] 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 647/680] 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 648/680] 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 649/680] 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 650/680] 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 651/680] 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 652/680] 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 653/680] 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 654/680] 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 655/680] [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 656/680] 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 657/680] 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 658/680] 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 659/680] 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 660/680] 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 661/680] 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 662/680] 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 664/680] 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 665/680] 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 666/680] 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 667/680] 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 668/680] 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 669/680] 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 670/680] 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 671/680] 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 672/680] 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 673/680] 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 674/680] 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 675/680] 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 676/680] 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 677/680] 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 678/680] 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 679/680] 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 4c1a9ebfa0768f02bcebc1d08b62d7274c13f138 Mon Sep 17 00:00:00 2001 From: Dan W Anderson Date: Wed, 5 Oct 2022 15:23:30 -0400 Subject: [PATCH 680/680] bump from upstream --- src/black/lines.py | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index 30622650d53..dd11dde7b55 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1,38 +1,29 @@ import itertools import sys from dataclasses import dataclass, field -from typing import ( - Callable, - Dict, - Iterator, - List, - Optional, - Sequence, - Tuple, - TypeVar, - cast, -) +from typing import Callable, Dict, Iterator, List, Optional, Sequence, Tuple, TypeVar, cast from black.brackets import DOT_PRIORITY, BracketTracker from black.mode import Mode, Preview 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, + 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") Index = int @@ -432,7 +423,7 @@ def __str__(self) -> str: if not self: return "\n" - indent = " " * self.depth + indent = " " * self.depth leaves = iter(self.leaves) first = next(leaves) res = f"{first.prefix}{indent}{first.value}"